Як один програміст Jocly підкував

— Учень Листоноші Стенлі — пробурмотів Гріш
— Сирота, сер. Дуже сумна історія… Хороший хлопчик, якщо його не злити,
… якщо ви розумієте, що я маю на увазі.
— Е… можливо. — сказав Мокрист і поспішно повернувся до Стенлі
— Отже, знаєш дещо про шпильках?..
— Нетсэр! — відповів Стенлі…
— Про шпильках я знаю все!  
 
                            сер Террі Пратчетт "Опочтарение".
 

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

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

Звичайно ж, така величезна бочка меду ніяк не могла обійтися без маленької ложки дьогтю (хоча, це кому як). Jocly не підтримує жодних DSLзразок ZRF або GDL і розробку доводиться вести на чистому і незамутненому JavaScript. Самі розробники визнають, що це більш трудомісткий підхід, але у нього є гігантський плюс — на JavaScript можна описати практично все що завгодно. Вірніше можна було б описати, якщо б сама Jocly не накладала пару обмежень. У поточній реалізації, підтримуються лише гри двох гравців з повною інформацією і без випадкових подій. Ці досить-таки суворі обмеження пов'язані, наскільки я розумію, з використовуваними алгоритмами AI (Alpha–beta і UCT Monte Carlo
 

Як би там не було, розробники, на мій погляд, зробили головне — відокремили модель ігри від її візуального подання. І те і інше можна писати окремо! Працюючи над моделлю, програміст може повністю відволіктися від питань її візуалізації, а впритул зайнявшись поданням, цілком здатний реалізувати, крім звичного 2D (єдино можливого в ZoG), ще й чесний 3D-інтерфейс. Це складно, але цілком здійсненне. При великому бажанні, навіть розробити свій власний дизайн фігур, намалювавши його в Blender-е.

Кращий спосіб зрозуміти — зробити щось, нехай навіть зовсім невелика, самому. Оскільки матеріал по кастомізації шахів на wiki авторів проекту вже був, я вирішив подивитися в бік шашок. Для перегляду деталей реалізації я використовував Jocly Inspector. В наявності були «Міжнародні», «Англійські», «Іспанські», «Бразильські шашки». Все що завгодно, крім "Російських шашок". Але якщо чогось немає — треба просто це зробити!

Будь-який додаток Jocly можна запустити на своєму комп'ютері (зі слів розробників, це єдиний спосіб запуску кастомізованих додатків). Зробити це допоможе Jocly jQuery plugin. Ось тут є непогана добірка прикладів, з демонстрацією його можливостей. Для початку роботи потрібно всього три файлу: jquery.jocly.min.js jquery.jocly.min.css і невеликий html-файл. Якщо робити все «по-правильному», необхідно покласти їх у каталог документів будь-якого Web-сервера (наприклад, Apache), але, як показала практика, якщо ви використовуєте FireFox, достатньо просто завантажити в нього наш 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-файлом, описаним в це керівництві, але з його більш повним варіантом працювати буде набагато зручніше. Тепер, необхідно включити в html-файл JSON-опис ігри. Тут є тонкий момент. Наш варіант гри буде називатися «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".

Настав час подумати про те, що ми будемо змінювати. Чим відрізняються «Російські шашки» від «Бразильських» (є в комплекті 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) і у всьому цьому зовсім не важко заблукати. Це вам не 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»?



ВідповідьЦе трохи заплутана тема. У більшості сучасних варіантів шашок, діє правило "Турецького удару": в процесі складного взяття, фігури супротивника не прибираються з дошки відразу, а лише позначаються як узяті. Забираються вони всі відразу, по завершенні ходу. Це правило діє практично скрізь, окрім… "Турецьких шашок"! У «Турецьких шашках», дамка являє собою грізну силу. Виконуючи взяття, вона «розчищає собі місце для наступних ходів. Всього одна дамка може з'їсти всю армію супротивника одним ходом!

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

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

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.