📜 ⬆️ ⬇️

As one programmer Jocly shoe

- Postman Stanley's apprentice - Grosz murmured
- Orphan, sir. A very sad story ... A good boy, if you don't annoy him,
... if you understand what I mean.
“Uh ... maybe.” - said Mokrist and hastily turned to Stanley
“So, do you know something about pins? ...”
- Netser! - answered Stanley ...
“About pins, I know everything!”

Sir Terry Pratchett " Respect ."

Back in 1998, Zillions of Games created a sensation among board game lovers, but it was not without flaws. Its main drawback was closeness. In order to play on top of a set of 48 games included in the demo kit, you had to pay money for activating the program. It was impossible to run ZoG on anything other than Windows (with some versions of this OS, there could be problems). Network mode was, but only on a local network or via a modem, the Web was not implied. Nothing can be done about it, it is a closed product! In addition, at present, it is practically not supported. I think that many will be happy to hear that there is an alternative free from the above disadvantages. Meet this Jocly .

Jocly developers were inspired by the example of Zillions of Games, but they chose a fundamentally different way. From the start, the Web was at the forefront. You can run Jocly-application in any modern browser, on any platform, including mobile! In most cases, you will be able to use a modern 3D interface, but if there are problems with compatibility, Jocly will switch to 2D itself. You can play with the computer, as well as with other people, view previously played games and even communicate with other players via video chat. Here you can see a brief description of the product features, as well as its comparison with the Zillions of Games.
')
Of course, such a huge barrel of honey could not do without a small spoonful of tar (although it was someone else). Jocly does not support any DSL , like ZRF or GDL, and development has to be done in pure and uncluttered JavaScript. The developers themselves admit that this is a more time-consuming approach, but it has a huge plus - in JavaScript you can describe almost anything. Rather, it would be possible to describe if Jocly herself did not impose a couple of restrictions. In the current implementation, only two- player games are supported with complete information and without random events. These rather severe restrictions are connected, as far as I understand, with the AI ​​algorithms used ( Alpha – beta and UCT Monte Carlo ).


Whatever it was, the developers, in my opinion, did the main thing - they separated the model of the game from its visual presentation . Both can be written separately! While working on the model, the programmer can completely distract from the questions of its visualization, and having come to grips with the presentation, it is able to implement, besides the usual 2D (the only possible one in ZoG), also an honest 3D interface. It is difficult , but quite realizable. With a great desire, you can even develop your own shape design, drawing it in Blender .

The best way to understand is to do something, even a very small one, on your own. Since the material on customization of chess on the wiki of the authors of the project was already, I decided to look in the direction of the checkers. To view the details of the implementation, I used Jocly Inspector . Available were "International", "English", "Spanish", "Brazilian checkers". Anything but the Russian Checkers . But if something is not - you just have to do it!

You can run any Jocly application on your computer (according to the developers, this is the only way to launch customized applications). This will help Jocly jQuery plugin . Here is a good selection of examples, with a demonstration of its capabilities. To get started, you need only three files: jquery.jocly.min.js , jquery.jocly.min.css and a small html file. If you do everything “according to the correct”, you need to put them in the document directory of any Web server (for example, Apache ), but, as practice has shown, if you use FireFox , you just need to upload our html file into it (with other browsers this focus is not worked).

Here is what it contains
<!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> 


For a simple launch of the game, it would be possible to get by with the minimal html file described in this manual, but it will be much more convenient to work with its fuller version. Now, you need to include in the html-file a JSON-description of the game. There is a subtle point. Our version of the game will be called “custom-drafts” (now, this name appears twice in the file). We can take the game description from the Jocly Inspector text field entirely, but if we change only a part of the files, this may be redundant. It is enough to describe only the part of the model in which we have made changes, Jocly will take the rest from his site, but in order for this to work, the name must be composed as follows: " drafts / custom-drafts". The part of the name before the slash is the name of a kind of “parental” game, from which all the missing will be taken. Again, this part of the name is not needed if the full JSON description is used.

Here is all we need
  <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> 


First of all, the description of the size of the board and the initial placement of the figures (the latter is not available in all Jocly games) is striking. It is a bit difficult to get used to the fact that the board is described as 4x8 (the fields that are not used in diagonal checkers are not described by the model), and all the indexes for placing figures start from zero. The following is a list of Boolean settings, sufficient (from the developers' point of view) to describe any checkered games. We will fill it up. It is not necessary to specify all the settings, I have compiled a complete list, solely for my convenience. It is important to describe in the " text / jocly-resources " all the files that we will give from our server. The file " checkersbase-custom-model.js " is the part of the model to which changes will be made. Initially, it is just a copy of the " checkersbase-model.js " file.

It is time to think about what we will change. What is the difference between Russian Checkers and Brazilian (available in the Jocly kit)? In fact, just two "little things." "Brazilian checkers" are played according to the rules of the "International" or "Polish Checkers", but on the board 8x8. There is a “majority rule” in them: from two or more variants of capture, the player must choose the one in which he “cuts down” the maximum number of opponent's checkers, regardless of their quality. In the "Russian drafts" option must be disabled. Everything is simple with this, the property is controlled by the boolean setting " captureLongestLine ".

By the way
It is interesting to see, as a rule, most implemented in checkers from Jocly. If the compound move is considered as a whole, the task becomes trivial. At the very end of the " Model.Board._GenerateMoves " method for generating moves, there is the following code fragment:

Selection from the list of generated moves
 ... 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; } ... 


We have a list of moves (in a particular view) and from it it is necessary to choose only those moves that take the maximum number of pieces (as interpreted by Jocly - consist of the maximum number of steps). In ZoG, with its concept of “partial” moves, we had to add the hardcode option " maximal captures " directly to the application in order to implement similar functionality.

More complications arise with another rule: if a saber became a lady during a series of takes, after transformation she continues “cutting” without stopping, already according to the rules of the woman. In International, as well as Brazilian checkers, another rule applies: if the checker was on the last line in the series of takes and can beat further in the role of simple checkers, then it continues the fight and does not turn ! Find in the code the place where the transformation occurs:

This is the '' Model.Board.ApplyMove '' method
 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); } } } 


You can see that the changes made, as well as the model of the board, moves, figures and other things, are far from intuitive. Many additional actions are performed in the code (such as calculating the Zobrist Hash ) and it’s not at all difficult to get lost. This is not ZRF! The essence of the changes is simple - we remember the fact of passing through the last horizontal (the first for black ones) and, if it took place, transform the figure as if at the end of the turn we were on the horizontal of transformation. Let's see how everything works:



It seems to be all right. We will not pay attention to the fact that the transformation occurs at the end of the course, and not in its process. In the framework of the current implementation of the model, the transformation of the figure in the middle of the course is not the best idea (everything will break, I checked)! But have we foreseen everything? Slightly change the position:



Yes, this is what we were afraid of. Having reached the last horizontal, the checker "does not know" that she has the right to "eat" like a lady! Let's try to explain to her. When making a move, it’s already a little late to make decisions about who is eating whom. It is logical to look for the right place in the method of generating moves, namely in the function " catchPieces ". In its last parameter is passed the flag " king ", showing that we are dealing with a woman. Let's try to change it when passing the last horizontal:

I did not immediately think of this
 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; } } } 


We are very lucky that the sign of the dam is passed as a parameter to the function. The move generator performs a tree traversal of all possible composite moves. If the character of the dam changed in the object of the figure, it would have to worry about rolling back the changes made in the model by the generator itself. Otherwise, the program might behave unpredictably . See how this is done in 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 ; 


Here, we copy the contents of the board into a temporary array (by calling $ CloneBoard ), then we select the “best” move, and then we delete the temporary state of the board ( $ DeallocateBoard ). And so - for each viewing level! Anyway, now everything works as it was intended:



Do not think that this is all over. In Jocly there is still something to break your head! See if you can say what's wrong with this Turkish Drafts video game?



Answer
This is a slightly confusing topic. In the majority of modern variants of checkers, the rule of " Turkish strike " is valid: in the process of complex taking, the enemy figures are not removed from the board immediately, but are only marked as taken. They are all taken immediately, at the end of the turn. This rule applies almost everywhere, except ... " Turkish drafts "! In "Turkish checkers", the lady is a formidable force. Performing a take, she “clears” her place for the subsequent moves. Only one lady can eat the entire army of the enemy in one move!

Judging by the video, in Jocly it is not. At the last step, it is clear that the lady cannot choose a longer chain of takes, since she is hampered by a previously taken piece that was not removed from the board. People far from board games may consider this circumstance unimportant, but none of the serious players will ever play Turkish Checkers by such rules! So far, I do not know how to fix it. The required fix is ​​more complicated than the customization described in this article. By making changes to the move generator, you can make the daisy “not see” the previously taken checkers, but, in addition, it is necessary to ensure that the lady can stop in the fields occupied by the checkers, including the possibility of completing the move in such a field. It is difficult and I am not ready to do it now, but maybe someone from the readers will offer a working solution?

We met with another interesting "engine" for the development of abstract board games. It has its limitations and the development process in it is not simple. But he has his own, completely deadly set of killer features! It is open, cross-platform, Web-oriented and, most importantly, it is still supported by developers! The project lives! Join him, and maybe he will live much longer than the legendary Zillions of Games.

Source: https://habr.com/ru/post/280334/


All Articles