- PVSM.RU - https://www.pvsm.ru -
— Ученик Почтальона Стэнли — пробормотал Грош
— Сирота, сэр. Очень печальная история… Хороший мальчик, если его не злить,
… если вы понимаете, что я имею в виду.
— Э… возможно. — сказал Мокрист и поспешно повернулся к Стэнли
— Итак, знаешь кое-что о булавках?…
— Нетсэр! — ответил Стэнли…
— О булавках я знаю всё!
сэр Терри Пратчетт "Опочтарение [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 не поддерживает каких либо 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".
...
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 = 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 ( -- )
-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»?
Судя по видео, в 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/
Нажмите здесь для печати.