- PVSM.RU - https://www.pvsm.ru -

Как один программист Jocly подковал

Как один программист Jocly подковал - 1— Ученик Почтальона Стэнли — пробормотал Грош
— Сирота, сэр. Очень печальная история… Хороший мальчик, если его не злить,
… если вы понимаете, что я имею в виду.
— Э… возможно. — сказал Мокрист и поспешно повернулся к Стэнли
— Итак, знаешь кое-что о булавках?…
— Нетсэр! — ответил Стэнли…
— О булавках я знаю всё!  
 
                            сэр Терри Пратчетт "Опочтарение [1]".
 

В далёком 1998 году, Zillions of Games [2] произвела фурор в среде любителей настольных игр, но она не была лишена недостатков. Главным её недостатком являлась закрытость. Для того, чтобы играть во что-то сверх набора из 48 игр, входящих в демо-комплект, приходилось платить деньги за активацию программы. Было невозможно запустить ZoG на чём-то кроме Windows (с некоторыми версиями этой ОС вполне могли возникнуть проблемы). Сетевой режим был, но только по локальной сети или через модем, Web не подразумевался. С этим ничего нельзя поделать, это закрытый продукт! Кроме того, в настоящее время, он практически не поддерживается. Я думаю, что многие будут рады услышать, что существует альтернатива, свободная от перечисленных выше недостатков. Знакомьтесь, это Jocly [3].

Разработчики Jocly вдохновлялись примером Zillions of Games, но пошли по принципиально иному пути. Во главу угла был с самого начала поставлен Web. Вы можете запустить Jocly-приложение в любом современном браузере, на любой платформе, включая мобильные! В большинстве случаев, вы сможете пользоваться современным 3D-интерфейсом, но если возникнут проблемы с совместимостью, Jocly самостоятельно переключится на 2D. Можно играть как с компьютером, так и с другими людьми, просматривать ранее сыгранные партии и даже общаться с другими игроками через видео-чат. Вот здесь [4] можно посмотреть краткое описание возможностей продукта, а также его сравнение [5] с Zillions of Games.

Как один программист Jocly подковал - 2Конечно же, такая огромная бочка мёда никак не могла обойтись без маленькой ложки дёгтя (хотя, это кому как). Jocly не поддерживает каких либо DSL [6], наподобие ZRF [7] или GDL [8] и разработку приходится вести на чистом и незамутнённом JavaScript. Сами разработчики признают [5], что это более трудоёмкий подход, но у него есть гигантский плюс — на JavaScript можно описать практически всё что угодно. Вернее можно было бы описать, если бы сама Jocly не накладывала пару ограничений. В текущей реализации, поддерживаются лишь игры двух игроков с полной информацией и без случайных событий. Эти довольно-таки суровые ограничения связаны, насколько я понимаю, с используемыми алгоритмами AI (Alpha–beta [9] и UCT [10] Monte Carlo [11]). 
 

Как бы там ни было, разработчики, на мой взгляд, сделали главное — отделили модель игры от её визуального представления. И то и другое можно писать отдельно! Работая над моделью, программист может полностью отвлечься от вопросов её визуализации, а вплотную занявшись представлением, вполне способен реализовать, помимо привычного 2D (единственно-возможного в ZoG), ещё и честный 3D-интерфейс. Это сложно [12], но вполне реализуемо. При большом желании, можно [13] даже разработать свой собственный дизайн фигур, нарисовав его в Blender [14]-е.

Лучший способ понять — сделать что-то, пусть даже совсем небольшое, самому. Поскольку материал [13] по кастомизации шахмат на wiki [15] авторов проекта уже был, я решил посмотреть в сторону шашек. Для просмотра деталей реализации я использовал Jocly Inspector [16]. В наличии имелись «Международные», «Английские», «Испанские», «Бразильские шашки». Всё что угодно, кроме "Русских шашек [17]". Но если чего-то нет — надо просто это сделать!

Любое приложение Jocly можно запустить на своём компьютере (со слов разработчиков, это единственный способ запуска кастомизированных приложений). Сделать это поможет Jocly jQuery plugin [18]. Вот здесь [19] имеется неплохая подборка примеров, с демонстрацией его возможностей. Для начала работы, требуется всего три файла: jquery.jocly.min.js [20], jquery.jocly.min.css [21] и небольшой html-файл. Если делать всё «по правильному», необходимо положить их в каталог документов любого Web-сервера (например Apache [22]), но, как показала практика, если вы используете FireFox [23], достаточно просто загрузить в него наш html-файл (с другими браузерами такой фокус не сработал).

Вот что он содержит

<!DOCTYPE html>
<html>
  <head>
    <meta charset='utf-8'>
	<link rel="stylesheet" href="jquery.jocly.min.css">
	<script src="http://code.jquery.com/jquery-1.10.1.min.js"></script>
	<script src="jquery.jocly.min.js"></script>

    <title>Jocly development stub web page</title>
    <script>
    	$(document).ready(function() {
    		$("#applet").jocly({});
    		$("#applet").jocly("localPlay","custom-draughts",{ });
    		$("#applet").jocly("setFeatures",{
    			notifyEnd: false,
    			hasEndSound: false,		
    		});
    		$("#options").joclyListener("listen","viewOptions",function(message) {
    			console.log("viewOptions",message);
    			$("#options-skin").hide().children("option").remove();
    			if(message.options.skin && message.skins && message.skins.length>0) {
    				message.skins.forEach(function(skin) {
    					$("<option/>").attr("value",skin.name).text(skin.title).appendTo($("#options-skin"));
    				});
    				$("#options-skin").show().val(message.options.skin);
    			}
    			$("#options-notation").hide();
    			if(message.options.notation!==undefined)
    				$("#options-notation").show().children("input").prop("checked",message.options.notation);
    			$("#options-moves").hide();
    			if(message.options.moves!==undefined)
    				$("#options-moves").show().children("input").prop("checked",message.options.moves);
    			$("#options-autocomplete").hide();
    			if(message.options.autocomplete!==undefined)
    				$("#options-autocomplete").show().children("input").prop("checked",message.options.autocomplete);
    			$("#options-sounds").hide();
    			if(message.options.sounds!==undefined)
    				$("#options-sounds").show().children("input").prop("checked",message.options.sounds);
    			$("#options").show();
    		});
    		$("#options").on("change",function() {
    			var options={};
    			if($("#options-skin").is(":visible")) 
    				options.skin=$("#options-skin").val();
    			if($("#options-notation").is(":visible"))
    				options.notation=$("#options-notation-input").prop("checked");
    			if($("#options-moves").is(":visible"))
    				options.moves=$("#options-moves-input").prop("checked");
    			if($("#options-autocomplete").is(":visible"))
    				options.autocomplete=$("#options-autocomplete-input").prop("checked");
    			if($("#options-sounds").is(":visible"))
    				options.sounds=$("#options-sounds-input").prop("checked");
    			$("#applet").jocly("viewOptions",options);
    		});
    		var defaultLevel=0;
    		$("#mode-panel").joclyListener("listen","players",function(message) {
    			console.warn("players",message);
    			function UpdatePlayer(player,key,levels) {
        			if(player.type=="computer") {
        				var select=$("#select-level-"+key);
        				select.empty();
        				for(var i=0;i<levels.length;i++)
        					$("<option/>").attr("value",i).text(levels[i].label).appendTo(select);
        				select.val(player.level);
        				$("#level-"+key).show();
        			} else
        				$("#level-"+key).hide();        				
    			}
    			UpdatePlayer(message.players[1],'a',message.levels);
    			UpdatePlayer(message.players[-1],'b',message.levels);
    			var modeSelect=$("#mode");
    			modeSelect.show();
    			if(message.players[1].type=="self" && message.players[-1].type=="self")
    				modeSelect.val("self-self");
    			else if(message.players[1].type=="self" && message.players[-1].type=="computer")
    				modeSelect.val("self-comp");
    			else if(message.players[1].type=="computer" && message.players[-1].type=="self")
    				modeSelect.val("comp-self");
    			else if(message.players[1].type=="computer" && message.players[-1].type=="computer")
    				modeSelect.val("comp-comp");
    			else
    				modeSelect.hide();
    			message.levels.forEach(function(level,index) {
    				if(level.isDefault)
    					defaultLevel=index;
    			});
    			$("#mode-panel").show();
    		});
    		$("#mode-panel").on("change",function() {
    			console.log("changed mode",$("#mode").val(),$("#select-level-a").val(),$("#select-level-b").val());
    			var players;
    			switch($("#mode").val()) {
    			case "self-self":
    				players={"1":{type:"self"},"-1":{type:"self"}};
    				break;
    			case "self-comp":
    				players={"1":{type:"self"},"-1":{type:"computer",level:$("#select-level-b").val() || defaultLevel}};
    				break;
    			case "comp-self":
    				players={"1":{type:"computer",level:$("#select-level-a").val() || defaultLevel},"-1":{type:"self"}};
    				break;
    			case "comp-comp":
    				players={"1":{type:"computer",level:$("#select-level-a").val() || defaultLevel},
    					"-1":{type:"computer",level:$("#select-level-b").val() || defaultLevel}};
    				break;
    			}
    			$("#applet").jocly("setPlayers",players);
    		});
    		$("#restart").on("click",function() {
    			$("#applet").jocly("restartGame");    			
    		});
    		$("#takeback").on("click",function() {
    			$("#applet").jocly("takeBack");    			
    		});
        	$("#fullscreen").on("click",function() {
        		$("#applet").joclyFullscreen();
        	});
    	});
    </script>
    <style type="text/css">
* {
	box-sizing: border-box;
}
body {
}
#container {
	width: 100%;
	display: table;
	table-layout: fixed;
}
#applet {
	display: table-cell;
	width: 60%;
}
#controls {
	display: table-cell;
	width: 33%;
	vertical-align: top;
	padding: 0 .5em 0 .5em;
}
.box {
	background-color: #f0f0f0;
	border: 2px solid #e0e0e0;
	border-radius: 1em;
	padding: 1em;
}
    </style>
    
    <script type="text/jocly-model-view" data-jocly-game="draughts/custom-draughts">
    <!-- Сюда включаем описание игры -->
    </script>
  </head>
  <body>
	<div id="container">
		<div id="applet"></div>
		<div id="controls">
	       	<div id="mode-panel" style="display: none;" class="box">
	       		<h3>Controls</h3>
	       		<button id="restart">Restart game</button><br/><br/>
	       		<button id="takeback">Take back</button><br/><br/>
	       		<select id="mode">
	       			<option value="self-self">Self / Self</option>
	       			<option value="self-comp">Self / Computer</option>
	       			<option value="comp-self">Computer / Self</option>
	       			<option value="comp-comp">Computer / Computer</option>
	       		</select><br/><br/>
	       		<label id="level-a" for="select-level-a">Computer(A) level<br/>
	        		<select id="select-level-a"></select><br/><br/>
	       		</label>
	       		<label id="level-b" for="select-level-b">Computer(B) level<br/>
	        		<select id="select-level-b"></select><br/><br/>
	       		</label>
	       		<button id="fullscreen">Full screen</button><br/><br/>
	       	</div>
	       	<br/>
	       	<div id="options" style="display: none;"  class="box">
	       		<h3>Options</h3>
	       		<select id="options-skin"></select><br/><br/>
	       		<label id="options-notation" for="options-notation-input">
	       			<input id="options-notation-input" type="checkbox"/> Notation<br/>
	       		</label>
	       		<label id="options-moves" for="options-moves-input">
	       			<input id="options-moves-input" type="checkbox"/> Show possible moves<br/>
	       		</label>
	       		<label id="options-autocomplete" for="options-autocomplete-input">
	       			<input id="options-autocomplete-input" type="checkbox"/> Auto-complete moves<br/>
	       		</label>
	       		<label id="options-sounds" for="options-sounds-input">
	       			<input id="options-sounds-input" type="checkbox"/> Sounds<br/>
	       		</label>
	       	</div>
		</div>
	</div>
  </body>
</html>

Для простого запуска игры, можно было бы обойтись минимальным html-файлом, описанным в этом [24] руководстве, но с его более полным [25] вариантом работать будет гораздо удобнее. Теперь, необходимо включить в html-файл JSON-описание [26] игры. Здесь есть тонкий момент. Наш вариант игры будет называться «custom-draughts» (сейчас, это имя встречается в файле дважды). Мы можем взять описание игры из текстового поля Jocly Inspector-а целиком, но если мы изменяем лишь часть файлов, это может быть излишним. Вполне достаточно описать лишь ту часть модели, в которую мы внесли изменения, остальное Jocly возьмёт со своего сайта, но для того, чтобы это работало, имя должно быть составлено следующим образом: "draughts/custom-draughts". Часть имени перед слэшем — имя, своего рода, «родительской» игры, из которой будет браться всё недостающее. Повторюсь, эта часть имени не нужна, если используется полное JSON-описание.

Здесь всё что нам понадобится

    <script type="text/jocly-model-view" data-jocly-game="draughts/custom-draughts">
    {
	"view": {
	    "js": [
	       "checkers-xd-view.js",
	       "draughts8-xd-view.js"
	    ]
	},
	"model": {
            "js": [
               "checkersbase-custom-model.js",
               "draughts-model.js"
            ],
	    "gameOptions": {
	      	"preventRepeat": true,
      		"width": 4,
      		"height": 8,
      		"initial": {
		        "a": [[0,0],[0,1],[0,2],[0,3],[1,0],[1,1],[1,2],[1,3],[2,0],[2,1],[2,2],[2,3]],
        		"b": [[7,0],[7,1],[7,2],[7,3],[6,0],[6,1],[6,2],[6,3],[5,0],[5,1],[5,2],[5,3]]
      		},
      		"variant": {
                        "compulsoryCatch": true,
                        "canStepBack": false,
                        "mustMoveForward": false,
		        "mustMoveForwardStrict": true,
                        "lastRowFreeze": false,
        		"lastRowCrown": true,
        		"captureLongestLine": true,
                        "kingCaptureShort": false,
                        "canCaptureBackward": true,
                        "longRangeKing": true,
                        "captureInstantRemove": false,
        		"lastRowFactor": 0.001
      		},
      		"uctTransposition": "state"
            }
	}
    }
    </script>

    <script type="text/jocly-resources" data-jocly-game="custom-draughts">
    {
        "checkersbase-custom-model.js": "checkersbase-custom-model.js"
    }
    </script>

В первую очередь, в глаза бросается описание размеров доски и начальной расстановки фигур (последнее имеется далеко не во всех Jocly-играх). Немного сложно привыкнуть к тому, что доска описывается как 4x8 (неиспользуемые в диагональных шашечных системах поля моделью не описываются), а все индексы размещения фигур начинаются с нуля. Далее следует список булевских настроек, достаточный (с точки зрения разработчиков) для описания любых шашечных игр. Мы его пополним. Не обязательно указывать все настройки, я составил полный список, исключительно для своего удобства. Важно описать в "text/jocly-resources" все файлы, которые мы будем отдавать со своего сервера. Файл "checkersbase-custom-model.js" — та часть модели, в которую будут вноситься изменения. Первоначально, это просто копия файла "checkersbase-model.js [27]".

Настало время подумать о том, что мы будем менять. Чем отличаются «Русские шашки» от «Бразильских» (имеющихся в комплекте Jocly)? На самом деле, всего двумя «мелочами». «Бразильские шашки» играются по правилам «Международных» или «Польских шашек», но на доске 8x8. В них действует «правило большинства»: из двух и более вариантов взятия игрок должен выбрать тот, при котором «срубит» максимальное количество шашек противника, независимо от их качества. В «Русских шашках» опцию необходимо отключить. С этим всё просто, свойство управляется булевской настройкой "captureLongestLine".

Кстати

Интересно посмотреть, как правило большинства реализовано в шашках от Jocly. Если составной ход рассматривается как единое целое, задача становится тривиальной. В самом конце метода генерации ходов "Model.Board._GenerateMoves" имеется следующий фрагмент кода:

Выбор из списка сгенерированных ходов

...
if(aGame.g.captureLongestLine) {
	var moves0=this.mMoves;
	var moves1=[];
	var bestLength=0;
	for(var i in moves0) {
		var move=moves0[i];
		if(move.pos.length==bestLength)
			moves1.push(move);
		else if(move.pos.length>bestLength) {
			moves1=[move];
			bestLength=move.pos.length;
		}
	}
	this.mMoves=moves1;
}
...

У нас есть список ходов (в том или ином представлении) и из него необходимо выбрать лишь те ходы, которые берут максимальное количество фигур (в интерпретации Jocly — состоят из максимального числа шагов). В ZoG, с её концепцией «частичных» ходов, пришлось добавлять хардкодную опцию "maximal captures" непосредственно в приложение, чтобы реализовать аналогичный функционал.

Больше сложностей возникает с другим правилом: если шашка стала дамкой в ходе серии взятий, после превращения она продолжает «рубку» без остановки, уже по правилам дамки. В «Международных», а также «Бразильских шашках», действует другое правило: если шашка оказалась на последней линии в ходе серии взятий и может бить дальше в роли простой шашки, то она продолжает бой и не превращается! Найдём в коде то место, где происходит превращение:

Это метод ''Model.Board.ApplyMove''

Model.Board.ApplyMove = function(aGame,move) {
+	var pieceCrowned=false;
	var WIDTH=aGame.mOptions.width;
	var HEIGHT=aGame.mOptions.height;
	var pos0=move.pos[0];
	var pIndex=this.board[pos0];
	var piece=this.pieces[pIndex];
	var player=piece.s;
	piece.l=pos0;
	var toBeRemoved={};
	this.zSign=aGame.zobrist.update(this.zSign,"board",piece.s+"/"+piece.t,piece.p);
	for(var i=1;i<move.pos.length;i++) {
		var pos=move.pos[i];
		this.board[piece.p]=-1;
		piece.p=pos;
+		if (aGame.g.russianCustom==true) {
+			var r=aGame.g.Coord[pos][0];
+			if((player==JocGame.PLAYER_A && r==HEIGHT-1) || (player==JocGame.PLAYER_B && r==0)) {
+				pieceCrowned=true;
+			}
+		}
		this.board[pos]=pIndex;
		var caught=move.capt[i];
		if(caught!=null) {
			if(this.board[caught]>=0)
				toBeRemoved[this.board[caught]]=true;
			this.board[caught]=-1;
		}
		pos0=pos;
	}
	this.zSign=aGame.zobrist.update(this.zSign,"board",piece.s+"/"+piece.t,pos);
	var plp=move.capt[move.capt.length-1]
	piece.plp=plp?plp:move.pos[move.pos.length-2];
	for(var index in toBeRemoved) {
		var piece0=this.pieces[index];
		var other=(1-piece0.s)/2;
		this.pCount[other]--;
		switch(piece0.t) {
			case 0: this.spCount[other]--; break;
			case 1: this.kpCount[other]--; break;
		}
		this.zSign=aGame.zobrist.update(this.zSign,"board",piece0.s+"/"+piece0.t,piece0.p);
		this.pieces[index]=null;
	}
	if(aGame.g.lastRowCrown && this.pieces[pIndex].t==0) {
		var r=aGame.g.Coord[move.pos[move.pos.length-1]][0];
-		if((player==JocGame.PLAYER_A && r==HEIGHT-1) || (player==JocGame.PLAYER_B && r==0)) {
+		if(pieceCrowned || (player==JocGame.PLAYER_A && r==HEIGHT-1) || (player==JocGame.PLAYER_B && r==0)) {
			var piece0=this.pieces[pIndex];
			piece0.t=1;
			var self=(1-player)/2;
			this.spCount[self]--;
			this.kpCount[self]++;
			this.zSign=aGame.zobrist.update(this.zSign,"board",piece0.s+"/0",piece0.p);
			this.zSign=aGame.zobrist.update(this.zSign,"board",piece0.s+"/1",piece0.p);
		}
	}
}

Можно заметить, что внесённые изменения, а также модель доски, ходов, фигур и прочего, далеки от интуитивных. В коде выполняется много дополнительных действий (типа вычисления Zobrist Hash [28]) и во всём этом совсем не трудно заблудиться. Это вам не ZRF! Суть изменений проста — мы запоминаем факт прохождения через последнюю горизонталь (первую для чёрных) и, если он имел место, превращаем фигуру так, как если бы в конце хода оказались на горизонтали превращения. Посмотрим, как всё работает:

Вроде бы всё правильно. Не будем обращать внимание на то, что превращение происходит по завершении хода, а не в его процессе. В рамках текущей реализации модели, превращение фигуры посреди хода — не лучшая идея (всё сломается, я проверял)! Но всё ли мы предусмотрели? Чуть-чуть изменим позицию:

Да, это то чего мы боялись. Дойдя до последней горизонтали, шашка «не знает», что дальше она имеет право «есть» как дамка! Попробуем ей объяснить. При выполнении хода, принимать решения о том, кто кого ест, уже немного поздно. Логично искать нужное место в методе генерации ходов, а именно в функции "catchPieces". В её последний параметр передаётся флаг "king", показывающий, что мы имеем дело с дамкой. Попробуем его изменить при прохождении последней горизонтали:

Я не сразу додумался до такого

function catchPieces(pos,poss,capts,dirs,king) {
	while(true) {
		var nextPoss=[];
		var nextCapts=[];
		var nextDirs=[];
		aGame.CheckersEachDirection(pos,function(pos0,dir) {
			var r;
			if(aGame.g.canCaptureBackward==false)
				r=aGame.g.Coord[pos][0];
			var dir0=aGame.Checkers2WaysDirections[dir];
+			if (aGame.g.russianCustom==true) {
+				if($this.board[pos0]>=0 && $this.pieces[$this.board[pos0]].s==-$this.mWho) {
+					var pp=aGame.g.Graph[pos0][dir];
+					if (aGame.g.Coord[pp]) {
+						var rr=aGame.g.Coord[pp][0];
+						var HEIGHT=aGame.mOptions.height;
+						if(($this.mWho==JocGame.PLAYER_A && rr==HEIGHT-1) || 
+							($this.mWho==JocGame.PLAYER_B && rr==0)) {
+							king=true;
+						}
+					}
+				}
+			}
			if(!king) {
				if($this.board[pos0]>=0 && $this.pieces[$this.board[pos0]].s==-$this.mWho) {
					var r0,forward;
					if(aGame.g.canCaptureBackward==false) {
						r0=aGame.g.Coord[pos0][0];
						forward=false;
						if(($this.mWho==JocGame.PLAYER_A && r0>=r) || 
							($this.mWho==JocGame.PLAYER_B && r0<=r))
								forward=true;
					}
					if(aGame.g.canCaptureBackward || forward==true) {
						var pos1=aGame.g.Graph[pos0][dir];
						if(pos1!=null && ($this.board[pos1]==-1 || pos1==poss[0])) {
							var keep=true;
							for(var i=0;i<dirs.length;i++)
								if((aGame.g.captureInstantRemove && capts[i]==pos0) ||
									(aGame.g.captureInstantRemove==false && 
										capts[i]==pos0 && dirs[i]==dir0)) {
									keep=false;
									break;
								}
							if(keep) {
								nextPoss.push(pos1);
								nextCapts.push(pos0);
								nextDirs.push(dir0);
							}
						}
					}
				}
			} else { // king
				if(aGame.g.longRangeKing)
					while($this.board[pos0]==-1 || 
						(aGame.g.king180deg && pos0!=null && capts.indexOf(pos0)>=0))
							pos0=aGame.g.Graph[pos0][dir];
				if(pos0!=null) {
					if($this.board[pos0]>=0 && $this.pieces[$this.board[pos0]].s==-$this.mWho) {
						var caught=pos0;
						pos0=aGame.g.Graph[pos0][dir];
						if(aGame.g.kingCaptureShort) {
							if($this.board[pos0]==-1 || pos0==poss[0]) {
								var keep=true;
								for(var i=0;i<dirs.length;i++)
									if(!aGame.g.king180deg) {
										if((aGame.g.captureInstantRemove && 
											capts[i]==caught) ||
											(aGame.g.captureInstantRemove==false && 
											capts[i]==caught && 
													dirs[i]==dir0)) {
											keep=false;
											break;
										}
									} else if(capts[i]==caught) {
										keep=false;
										break;												
									}
								if(keep) {
									nextPoss.push(pos0);
									nextCapts.push(caught);
									nextDirs.push(dir0);
								}
								pos0=aGame.g.Graph[pos0][dir];
							}
						} else {
							while($this.board[pos0]==-1 || pos0==poss[0]) {
								var keep=true;
								for(var i=0;i<dirs.length;i++)
									if((aGame.g.captureInstantRemove && capts[i]==caught) ||
											(aGame.g.captureInstantRemove==false && 
											capts[i]==caught && dirs[i]==dir0)) {
										keep=false;
										break;
									}
								if(keep) {
									nextPoss.push(pos0);
									nextCapts.push(caught);
									nextDirs.push(dir0);
								}
								pos0=aGame.g.Graph[pos0][dir];
							}
						}
					}
				}
			}
			return true;
		});
		if(nextPoss.length==0) {
			if(poss.length>1)
				$this.mMoves.push({ pos: poss, capt: capts });
			break;
		}
		if(!aGame.g.compulsoryCatch && poss.length>1) {
			var poss1=[];
			for(var i=0;i<poss.length;i++)
				poss1.push(poss[i]);
			var capts1=[];
			for(var i=0;i<capts.length;i++)
				capts1.push(capts[i]);
			$this.mMoves.push({ pos: poss1, capt: capts1 });
		}
		if(nextPoss.length==1) {
			pos=nextPoss[0];
			poss.push(pos);
			capts.push(nextCapts[0]);
			dirs.push(nextDirs[0]);
		} else {
			for(var i=0;i<nextPoss.length;i++) {
				var poss1=[];
				for(var j=0;j<poss.length;j++)
					poss1.push(poss[j]);
				poss1.push(nextPoss[i]);
				var capts1=[];
				for(var j=0;j<capts.length;j++)
					capts1.push(capts[j]);
				capts1.push(nextCapts[i]);
				var dirs1=[];
				for(var j=0;j<dirs.length;j++)
					dirs1.push(dirs[j]);
				dirs1.push(nextDirs[i]);
				catchPieces(nextPoss[i],poss1,capts1,dirs1,king);
			}
			break;
		}
	}
}

Нам здорово повезло с тем, что признак дамки передаётся в качестве параметра функции. Генератор ходов выполняет обход дерева всех возможных составных ходов. Если бы признак дамки изменялся в объекте фигуры, пришлось бы заботиться об откате изменений, выполненных в модели самим генератором. В противном случае, программа могла бы вести себя непредсказуемо. Посмотрите, как это делается в Axiom:

Custom Engine

: Custom-Engine ( -- )
	-10000 BestScore !
	0 Nodes !
	$FirstMove
	BEGIN
		$CloneBoard
		DUP $MoveString 
		CurrentMove!
		DUP .moveCFA EXECUTE
		MaxDepth Depth !
		0 EvalCount !
		BestScore @ 10000 turn-offset next-turn-offset Score
		0 5 $RAND-WITHIN +
		BestScore @ OVER <
		IF
			DUP BestScore !
			Score!
			0 Depth!
			DUP $MoveString BestMove!
		ELSE
			DROP
		ENDIF
		$DeallocateBoard
		Nodes ++
		Nodes @ Nodes!
		$Yield
		$NextMove
		DUP NOT
	UNTIL
	DROP
;

Здесь, мы копируем содержимое доски во временный массив (вызовом $CloneBoard), затем выбираем «лучший» ход, после чего удаляем временное состояние доски ($DeallocateBoard). И так — для каждого уровня просмотра! Как бы там ни было, теперь всё работает, как и было задумано:

Не стоит думать, что на этом всё закончено. В Jocly ещё есть, над чем поломать голову! Посмотрите, сможете ли вы сказать, что не так на этом видео игры в «Turkish Draughts»?

Ответ

Это немного запутанная тема. В большинстве современных вариантов шашек, действует правило "Турецкого удара [29]": в процессе сложного взятия, фигуры противника не убираются с доски сразу, а лишь помечаются как взятые. Забираются они все сразу, по завершении хода. Это правило действует практически везде, кроме… "Турецких шашек [30]"! В «Турецких шашках», дамка представляет собой грозную силу. Выполняя взятие, она «расчищает» себе место для последующих ходов. Всего одна дамка может съесть всю армию противника одним ходом!

Судя по видео, в Jocly это не так. На последнем шаге видно, что дамка не может выбрать более длинную цепочку взятий, поскольку ей мешает ранее взятая шашка, не убранная с доски. Люди, далёкие от настольных игр, могут счесть это обстоятельство несущественным, но ни один из серьёзных игроков никогда не станет играть в «Турецкие шашки» по таким правилам! Пока, я не знаю как это исправить. Требуемое исправление сложнее, чем кастомизация, описанная в этой статье. Внеся изменения в генератор ходов, можно заставить дамку «не видеть» ранее взятые шашки, но, кроме того, необходимо обеспечить дамке возможность остановки на полях, занятых взятыми шашками, в том числе и возможность завершения хода на таком поле. Это сложно и я не готов сейчас этим заниматься, но возможно кто-то из читателей предложит работающее решение?

Мы познакомились с ещё одним интересным «движком» для разработки абстрактных настольных игр. У него есть свои ограничения и процесс разработки в нём не прост. Но у него есть свой, совершенно убийственный набор киллер-фич! Он открытый, кросс-платформенный, Web-ориентированный и, самое главное, он всё ещё поддерживается разработчиками! Проект живёт! Включайтесь в него, и, возможно, он будет жить гораздо дольше чем легендарная Zillions of Games.

Автор: GlukKazan

Источник [31]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/javascript/116619

Ссылки в тексте:

[1] Опочтарение: https://ru.wikipedia.org/wiki/%D0%9E%D0%BF%D0%BE%D1%87%D1%82%D0%B0%D1%80%D0%B5%D0%BD%D0%B8%D0%B5

[2] Zillions of Games: http://www.zillions-of-games.com/

[3] Jocly: https://www.jocly.com/#/about

[4] здесь: http://wiki.jocly.com/index.php/All_you_can_do_with_Jocly

[5] сравнение: http://wiki.jocly.com/index.php/Comparison_with_Zillions_of_Games

[6] DSL: https://ru.wikipedia.org/wiki/%D0%9F%D1%80%D0%B5%D0%B4%D0%BC%D0%B5%D1%82%D0%BD%D0%BE-%D0%BE%D1%80%D0%B8%D0%B5%D0%BD%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%BD%D1%8B%D0%B9_%D1%8F%D0%B7%D1%8B%D0%BA

[7] ZRF: https://ru.wikipedia.org/wiki/Zillions_of_Games

[8] GDL: https://en.wikipedia.org/wiki/Game_Description_Language

[9] Alpha–beta: https://en.wikipedia.org/wiki/Alpha%E2%80%93beta_pruning

[10] UCT: http://wiki.jocly.com/index.php/Jocly_UCT

[11] Monte Carlo: https://en.wikipedia.org/wiki/Monte_Carlo_tree_search

[12] сложно: http://wiki.jocly.com/index.php/XDView

[13] можно: http://wiki.jocly.com/index.php/Changing_the_pieces_with_my_own_set

[14] Blender: http://www.blender.org/

[15] wiki: http://wiki.jocly.com

[16] Jocly Inspector: http://embed.jocly.net/jocly/plazza/inspector#/

[17] Русских шашек: https://ru.wikipedia.org/wiki/%D0%A0%D1%83%D1%81%D1%81%D0%BA%D0%B8%D0%B5_%D1%88%D0%B0%D1%88%D0%BA%D0%B8

[18] Jocly jQuery plugin: https://github.com/mi-g/jquery-jocly

[19] здесь: http://mi-g.github.io/jquery-jocly/index.html

[20] jquery.jocly.min.js: https://raw.githubusercontent.com/mi-g/jquery-jocly/master/jquery.jocly.min.js

[21] jquery.jocly.min.css: https://raw.githubusercontent.com/mi-g/jquery-jocly/master/jquery.jocly.min.css

[22] Apache: https://httpd.apache.org/

[23] FireFox: https://www.mozilla.org/ru/firefox/new/

[24] этом: http://wiki.jocly.com/index.php/Embed_a_game

[25] более полным: https://github.com/mi-g/jquery-jocly/blob/master/dev-stub.html

[26] JSON-описание: http://wiki.jocly.com/index.php/Jocly_Basics

[27] checkersbase-model.js: http://embed.jocly.net/jocly/plazza/file-access?game=draughts&file=checkersbase-model.js

[28] Zobrist Hash: https://en.wikipedia.org/wiki/Zobrist_hashing

[29] Турецкого удара: https://ru.wikipedia.org/wiki/%D0%A2%D1%83%D1%80%D0%B5%D1%86%D0%BA%D0%B8%D0%B9_%D1%83%D0%B4%D0%B0%D1%80

[30] Турецких шашек: https://ru.wikipedia.org/wiki/%D0%A2%D1%83%D1%80%D0%B5%D1%86%D0%BA%D0%B8%D0%B5_%D1%88%D0%B0%D1%88%D0%BA%D0%B8

[31] Источник: https://habrahabr.ru/post/280334/