📜 ⬆️ ⬇️

Dagaz: evolution instead of revolution

image In this world of what we would like NO!
We believe in the power to change it YES!

Yuri Shevchuk


')
Those of you who have read my articles should be aware that I have been studying the Zillions of Games metagame system for quite some time. For all this time, I developed a little less than fifty games and learned this platform up and down. My goal is to develop a similar (and preferably more functional) open source system. On the progress of this work and I want to tell.

In the image and likeness


As I said, I understand very well how Zillions of Games works. The absence of its source codes does not bother me, since I am not going to engage in the porting of this product. We are talking about the development of a new system from scratch, taking into account the merits (and even more deficiencies) of all the metagame platforms that I know at the moment. I will list them:


All these products work and do exactly what they are intended for - they help, with the expenditure of more or less effort, to create computer implementations of various board games. It's not just about Checkers and Chess! The number and (most importantly) the variety of games already created exceeds all expectations. This is the main advantage of metagame systems - a working prototype of a new and rather complicated board game can be created in just a couple of hours!

A spoon of tar
Their main drawback is also obvious. No one universal game "engine" will ever be equal (in performance) to programs specialized, focused on one and only one board game. The “intelligence” of bots that are designed to make a company to a human player who is alone is directly related to this. All universal gaming systems play very poorly, but since we are usually talking about rather exotic games, this is not a very big problem. It is unlikely that the program will be lucky to meet with a person playing, for example, in Chu Shogi at the level of a grandmaster.

In addition to this common flaw (as well as the fatal flaw associated with the closeness of the source code), each of these projects has individual features. For example , Zillions of Games uses a lispo-like DSL , which greatly simplifies the process of describing board games, but somewhat limits the functionality available to the developer. With it, you can really implement a lot , but not everything. Some games, such as Ritmomachia or Kauri , are absolutely impossible to develop on pure ZRF . Others, like " Ko Shogi " or " Gwangsanghui ", can be done, but in such a complex way that their performance (and, consequently, the AI's "intelligence") is significantly affected.

The Axiom Development Kit extension emerged as an attempt to improve the Zillions of Games. Since this library operates with numbers (and not just boolean flags, like Zillions of Games), games such as Ritmochia become realizable , but the development process itself resembles a nightmare in places (I wrote a little about this). As a DSL, Axiom uses Forth Script (a subset of the Fort language) and this language (and most importantly debugging programs on it) is indeed much more complex than a warm and lamp ZRF. In addition, you can do with it, not all. The development of such games as Tavreli or Kauri mentioned above is still not possible.

I can tell little about LUD (since I have never seen this product live), and as for Jocly, the disadvantage of this system (in my opinion) is the complete rejection of using any DSL to describe games. In fact, this is an MVC framework for developing JavaScript board games. Even the introduction of rather trivial changes to already developed games becomes a very laborious process . Games created by the authors themselves are also not without serious mistakes (I associate this with the complexity of the development process). For example, in Alquerque , situations arise in which the same figures are “taken” several times per turn, and in Turkish Checkers , the Turkish Strike rule is wrong, the main thing that distinguishes this game from other checkers systems .

Acquaintance with Jocly prompted me to revise some decisions. For further development, I firmly decided to use JavaScript, since this is obviously the easiest way to create an extensible and cross-platform system with a modern interface. The single-threading is a bit scary, but in fact this moment is important only for AI (and it’s not easy to use multithreading), and we have (for ourselves) found out that AI is not the strongest side of metagame systems.

On the other hand, the need for a certain DSL to describe the most routine moments of board games is quite obvious to me. Directly using JavaScript to develop the entire gaming model gives the process unprecedented flexibility, but requires diligence and concentration (and, as experience has shown, even their presence does not help much). Ideally, I would like to ensure compatibility with the base ZRF, in order to be able to run in the new system, if not all two and a half thousand games , then at least a significant part of them. Here is what Jocly developers write about it:

In ZoG, games are described in a lisp-based language called ZRF. This is where the ZRF has been defined. The Jocly Approach It is a good idea.

In theory, it would be possible to write a ZRF interpreter in Javascript. If you are willing to develop that kind of tool, let us know.

I decided to move along this path, concentrating, however, not on interpretation, but on a kind of “compilation” of the ZRF file in the game description for Jocly. Constant parsing of a text file, even if it contains a very simple description of the game, in a language resembling Lisp is not a task that I would like to engage in JavaScript.

Details
I decided to create an application that turns the original zrf file containing the game description into a form suitable for loading into the Jocly model. For example, instead of this file (you can use the Jocly Inspector to view all open texts of the Jocly platform). Of course, an interlayer was needed that could “glue” this description with the Jocly model. Z2J-translator once performs the work that I would not want to engage in a JavaScript application all the time. For example:

The following description of the game board
(grid (start-rectangle 6 6 55 55) (dimensions ("a/b/c/d/e/f/g/h" (50 0)) ; files ("8/7/6/5/4/3/2/1" (0 50)) ; ranks ) (directions (n 0 -1) (s 0 1) (e 1 0) (w -1 0)) ) 

Turns into ...
  design.addDirection("w"); design.addDirection("e"); design.addDirection("s"); design.addDirection("n"); design.addPosition("a8", [0, 1, 8, 0]); design.addPosition("b8", [-1, 1, 8, 0]); ... 

In fact, this is a description of the graph, the vertices of which are the individual positions on the board, and the arcs (oriented) are the directions in which the figures can move. The integers specified in the arrays associated with the vertices of the graph represent offsets within the linear array of all positions used in the game (a zero offset value indicates the absence of an arc). When using this approach, navigation in any direction is reduced to one arithmetic addition:

ZrfDesign.navigate
 ZrfDesign.prototype.navigate = function(aPlayer, aPos, aDir) { var dir = aDir; if (typeof this.players[aPlayer] !== "undefined") { dir = this.players[aPlayer][aDir]; } if (this.positions[aPos][dir] !== 0) { return aPos + this.positions[aPos][dir]; } else { return null; } } 

Well, there is still an optional change in the direction of movement, depending on the player performing the move (the so-called "symmetry"), which allows, for example, to describe the movement of all pawns (both black and white) as moving "north". If the move will be black, the direction will be changed to “southern” automatically. "Zero symmetry" allows you to describe "opposed" movement for each direction (in many games this is useful):
 design.addPlayer("White", [1, 0, 3, 2]); 
The rules for moving figures are more complicated.

Move checkers
 (define checker-shift ( $1 (verify empty?) (if (in-zone? promotion) (add King) else add ) )) 

Turns into ...
  design.addCommand(1, ZRF.FUNCTION, 24); // from design.addCommand(1, ZRF.PARAM, 0); // $1 design.addCommand(1, ZRF.FUNCTION, 22); // navigate design.addCommand(1, ZRF.FUNCTION, 1); // empty? design.addCommand(1, ZRF.FUNCTION, 20); // verify design.addCommand(1, ZRF.IN_ZONE, 0); // promotion design.addCommand(1, ZRF.FUNCTION, 0); // not design.addCommand(1, ZRF.IF, 4); design.addCommand(1, ZRF.PROMOTE, 1); // King design.addCommand(1, ZRF.FUNCTION, 25); // to design.addCommand(1, ZRF.JUMP, 2); design.addCommand(1, ZRF.FUNCTION, 25); // to design.addCommand(1, ZRF.FUNCTION, 28); // end 

These are the commands of the stack machine, each of which is very simple. For example, the PARAM command gets a numeric value from an array of parameters attached to the move pattern (set of commands) and pushes it onto the stack. It allows you to parameterize stroke patterns, passing the direction of movement in the parameters:

Description of the figure
  design.addPiece("Man", 0); design.addMove(0, 0, [3, 3], 0); design.addMove(0, 0, [0, 0], 0); design.addMove(0, 0, [1, 1], 0); design.addMove(0, 1, [3], 1); design.addMove(0, 1, [0], 1); design.addMove(0, 1, [1], 1); 

The third parameter is the “mode” of the move - a numerical value that allows, among other things, to separate the “quiet” moves (in checkers) from the taking of the moves. The whole triple (pattern + parameters + move mode) is a complete description of one of the possible moves performed by the figure.

Jocly is built according to the classic MVC scheme. To develop a new game, you need to write its model and presentation. The model determines the rules of the game, and the presentation - how the game will be shown to the user. The controller, written by the developers, takes care of the rest (including the bots wired into it).


The architecture of the universal model implemented by Z2J is also not very complicated. The basis is the Design component, which contains an immutable description of the rules of the game. The state of the game (placement of pieces on the board) is stored in instances of the Board class. The data of these components also do not change. By executing a move (applying the Move object to the Board ), we create a new state. The old remains the same!


To generate a move (create a Move object), the current state of the Board is used , but it alone is not enough to realize all the capabilities of the ZRF. In the process of generating a turn, ZRF can use variables (flags and position flags) that are not part of the game state. All this, as well as the logic of the execution of the stack machine commands, is handled by the Move Generator . In short, this is the architecture of the zrf-model.js module .

The devil is in the details


So, I was going to embed my model (zrf-model.js), configured by the compilation of Turkish Checkers , instead of the Jocly model , and try to run all this without making any changes to the game view . Looking back, I understand that the idea was adventurous (why - I’ll tell you below), but that’s what I started from. It was required a little from the model:

  1. Keeping the current state of the game
  2. Generation of all moves allowed for the current game state
  3. Changing the state of the game by applying one of the generated moves to it

The difficulty was that the move, far from always, boils down to a simple movement of one of the pieces on the board. In the most general form, a move consists of a sequence of the following elementary actions:


For example, taking a piece in checkers consists of one moving one's own figure and taking an opponent's piece (in this case, the taking is not “chess”, since the position with which it is performed does not coincide with the final position of the piece's movement), but moves in such games as " Renju "consist of single drops of figures on the board. One should not think that when performing a move only one figure can move! So, when casting in chess, the rook and the king move simultaneously, within the same indivisible move.

How it works
Generating a move comes down to forming a list of elementary actions that are performed in the correct sequence. This is simply a sequential interpretation of the commands of the stack machine:

ZrfMoveGenerator.generate
 ZrfMoveGenerator.prototype.generate = function() { this.cmd = 0; while (this.cmd < this.template.commands.length) { var r = (this.template.commands[this.cmd++])(this); if (r === null) break; this.cmd += r; if (this.cmd < 0) break; } } 

If we omit the details related to the checks of the necessary conditions (not finding the fields under the check, immobility of the figures before the execution of the turn, etc.), the short castling code, expressed in ZRF, may look like this:

Castling
 (define OO ( ee to e cascade ww add )) 

Turns into ...
 design.addCommand(0, ZRF.FUNCTION, 24); // from design.addCommand(0, ZRF.PARAM, 0); // e design.addCommand(0, ZRF.FUNCTION, 22); // navigate design.addCommand(0, ZRF.PARAM, 1); // e design.addCommand(0, ZRF.FUNCTION, 22); // navigate design.addCommand(0, ZRF.FUNCTION, 25); // to design.addCommand(0, ZRF.PARAM, 2); // e design.addCommand(0, ZRF.FUNCTION, 22); // navigate design.addCommand(0, ZRF.FUNCTION, 24); // from design.addCommand(0, ZRF.PARAM, 3); // w design.addCommand(0, ZRF.FUNCTION, 22); // navigate design.addCommand(0, ZRF.PARAM, 4); // w design.addCommand(0, ZRF.FUNCTION, 22); // navigate design.addCommand(0, ZRF.FUNCTION, 25); // to design.addCommand(0, ZRF.FUNCTION, 28); // end 

In addition to parameterized navigation, it all comes down to moving figures taken from the from command (implicitly performed at the beginning of the turn and when executing the cascade command) to the field indicated by the to command (also generated implicitly). The command handler itself looks elementary:

Model.Move.ZRF_TO
 Model.Game.functions[Model.Move.ZRF_TO] = function(aGen) { if (aGen.pos === null) { return null; } if (typeof aGen.piece === "undefined") { return null; } aGen.movePiece(aGen.from, aGen.pos, aGen.piece); delete aGen.from; delete aGen.piece; return 0; } ZrfMoveGenerator.prototype.movePiece = function(aFrom, aTo, aPiece) { this.move.movePiece(aFrom, aTo, aPiece, this.level); if (aFrom !== aTo) { this.setPiece(aFrom, null); } this.setPiece(aTo, aPiece); } ZrfMove.prototype.movePiece = function(from, to, piece, part) { this.actions.push([ from, to, piece, part ]); } 


But all this is only part of the problem! In checkers, the figure can (and moreover, is obliged) to perform several takes "along the chain." Until all captures are completed, the move is not transferred to another player. From the point of view of the model and for AI, this is one move! With the controller and the presentation is a little more complicated. In the user interface of the game, each checker take (partial move) must be performed separately. The user (player) must be able to choose one or another partial move at each stage of the execution of a long composite move.

Of course, this is not the only possible approach.
In Zillions of Games, each partial move is considered a separate move. This simplifies the user interface, but, on the other hand, not only complicates the life of the AI, but also leads to more serious problems.


It shows the sequence of positions that arise when performing a composite move in the game " Mana ", developed by Claude Leroy in 2005. According to the rules of the game, white Damyo must perform three consecutive steps, horizontally or vertically, to the next empty position. At the same time, all steps must be taken and the figure is prohibited to return to previously completed positions. As it is easy to see, a piece can drive itself into a “dead end” by choosing the wrong sequence of partial moves. In Zillions of Games, this problem is insoluble!

With Checkers, everything is also not easy. In almost all traditional drafts games (with the exception of Fanorona ), the player must continue to take, while there is such a possibility. This means that when performing a partial move containing a take, we still do not know whether it completes a valid composite move or not.

Of course, this can be fought ...
but it already reminds a lot ...
'' sunset sun manually ''
 (define checker-captured-find mark (if (on-board? $1) $1 (if (and enemy? (on-board? $1) (empty? $1) (not captured?)) (set-flag more-captures true) ) ) back ) (define king-captured-find mark (while (and (on-board? $1) (empty? $1)) $1 ) (if (on-board? $1) $1 (if (and enemy? (empty? $1) (not captured?)) (set-flag more-captures true) ) ) back ) (define checker-jump ( (verify (not captured?)) $1 (verify enemy?) (verify (not captured?)) $1 (verify empty?) (set-flag more-captures false) (if (in-zone? promotion) (king-captured-find $1) (king-captured-find $2) (king-captured-find $3) else (checker-captured-find $1) (checker-captured-find $2) (checker-captured-find $3) ) (if (flag? more-captures) (opposite $1) (markit) $1 ) (if (not (flag? more-captures)) (opposite $1) (if enemy? capture ) $1 (capture-all) ) (if (in-zone? promotion) (if (flag? more-captures) (add-partial King jumptype) else (add-partial King notype) ) else (if (flag? more-captures) (add-partial jumptype) else (add-partial notype) ) ) )) 

Moreover, in many checkers games, such as International Checkers , there is a “majority rule”, according to which the player must take the maximum possible number of opponent pieces. In some games it is specified that taking the greatest number of ladies should be considered a priority. Considering each partial move separately, Zillions of Games is forced to resort to the "magic of options":

  • (option "pass partial" true) - allows interrupting the chain of captures
  • (option "maximal captures" true) - take the maximum number of shapes
  • (option "maximal captures" 2) - take the maximum number of queens (if the number of queens taken is the same, take the maximum number of pieces)

And now, just compare this hardcode with that ...

how Jocly performs a similar test
  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; } 

When the entire composite move is available in its entirety, nothing prevents you from simply counting the number of takes that it takes.

Compound stroke generation is the simplest application of the ZrfMoveGenerator. Each copy of the generator forms its partial stroke, and the partial moves themselves concatenate into a “chain” of the composite stroke. Unfortunately, this is not the only way ZRF can use to determine moves. Consider a very simple case describing a piece moving through empty fields in one direction (such as Elephant , Rook and Queen in Chess ):

Chess Rider
 (define slide ( $1 (while empty? add $1) (verify enemy?) add )) 

You can see that the add command, completing the formation of the move, is used in the body of the loop. This means that the figure can stop on any empty field, on the way to the enemy figure (and this will be considered a correct move). Of course, such a cycle can be eliminated by rewriting the definition:

In some ZRF games you have to use this method.
 (define slide-1 ( $1 (verify enemy?) add )) (define slide-2 ( $1 (verify empty?) $1 (verify enemy?) add )) (define slide-3 ( $1 (verify empty?) $1 (verify empty?) $1 (verify enemy?) add )) ... 

The add command, executed in the body of the loop, leads to the formation of a non-deterministic move. The figure may stop or go further. For ZrfMoveGenerator, this means the need for cloning. The generator creates a complete copy of its state and pushes it onto the stack for subsequent generation, after which the current copy completes the formation of the turn. Here's what it looks like:

Moving dyke
 (define king-shift ( $1 (while empty? add $1 ) )) 

turns into ...
  design.addCommand(3, ZRF.FUNCTION, 24); // from design.addCommand(3, ZRF.PARAM, 0); // $1 design.addCommand(3, ZRF.FUNCTION, 22); // navigate design.addCommand(3, ZRF.FUNCTION, 1); // empty? design.addCommand(3, ZRF.FUNCTION, 0); // not design.addCommand(3, ZRF.IF, 7); design.addCommand(3, ZRF.FORK, 3); design.addCommand(3, ZRF.FUNCTION, 25); // to design.addCommand(3, ZRF.FUNCTION, 28); // end design.addCommand(3, ZRF.PARAM, 1); // $2 design.addCommand(3, ZRF.FUNCTION, 22); // navigate design.addCommand(3, ZRF.JUMP, -8); design.addCommand(3, ZRF.FUNCTION, 28); // end 

The FORK command clones the progress generator along with its entire current state and works as a conditional transition. In the generated generator, the control will go to the next command, and the parent will transfer control to the number of steps specified by the parameter (yes, this is very much like creating a process in Linux).

Burden of compatibility
In order for ZRF game descriptions to work after their “translation” in JavaScript, it is not enough just to execute similar commands in the same order. The semantics of operations (in terms of interaction with the state of the board) must fully coincide with the one used by Zillions of Games. In order for you to imagine the whole degree of complexity of the question, I will briefly list the main points:

  • During the course generation, the board is available in the state it was at the time of the generation start. The moved figure is not removed from the source field and, of course, is not set to the current one. This requirement is understandable (especially if we recall the immobility of the board), but in real life it is extremely uncomfortable.
  • The state of flags (bit variables) and positional flags (bit variables attached to specific positions) is available only in the course generation process. In the case of Zillions of Games, which treats each partial move as a separate move, this greatly reduces their usefulness, but we must provide similar semantics for everything to work.
  • The storage of attributes (named bit flags attached to figures) is not limited to stroke generation. Attributes are part of the board's status. By the way, the figures themselves are also immutable, changing any of the attributes with it, we create a new figure.
  • Since the state of the board is available at the moment of starting the generation of a move, it is possible to read the attribute only at the place of the initial location of the figure, but if we want to change the attribute, then it should be done at the position where the figure completes its movement (that is, it turns out at the moment of completion of the move). If you change an attribute on another field (for example, on the source field), a fatal error will not occur. The value is simply not established.
  • Cascade moves are not transmitted when cloning moves. Rather, they are transmitted, but only if the option " discard cascades " is disabled . Never seen a game where it is used!
  • Intermediate captures and drops of figures are also not transferred to the cloned move. As a result, taking a woman in Russian Checkers turns into a real puzzle (from the point of possible completion of the move with the add command performed in the cycle, you must move back to take the previously jumped enemy figure.
  • We cannot take a figure whose type, attribute value or owner has changed on the same turn! It looks more like a bug, but you can't throw a word out of a song.
  • If the turn ends in a position containing a piece, a “chess take” is performed automatically. If the capture command is explicitly called on the same field, the figure that performed the move will be deleted (this way you can make kamikaze figures). Similarly, using the create command, you can change the type and the owner of the shape.
  • If the delayed capture option is enabled, if you continue the turn, all the captures of the pieces should be moved to the last partial stroke of the compound stroke. This option, for obvious reasons, is not in ZRF, but when it is needed, it is so lacking! The implementation of the rule of " Turkish strike " in ZRF is a form of torment! Fortunately, we are considering a composite move entirely. Why not implement such a useful option?

This is not a complete list. Just the first thing that came to mind. In addition, it is necessary to implement a loop to iterate through all of their pieces that can perform a move (in Zillions of Games, the player can move only his pieces), as well as all the empty fields to which the piece can be “reset”.

All together it looks something like this
 var CompleteMove = function(board, gen) { var t = 1; if (Model.Game.passPartial === true) { t = 2; } for (var pos in board.pieces) { var piece = board.pieces[pos]; if ((piece.player === board.player) || (Model.Game.sharedPieces === true)) { for (var move in Model.Game.design.pieces[piece.type]) { if ((move.type === 0) && (move.mode === gen.mode)) { var g = f.copy(move.template, move.params); if (t > 0) { g.moveType = t; g.generate(); if (g.moveType === 0) { CompleteMove(board, g); } } else { board.addFork(g); } t = 0; } } } } } ZrfBoard.prototype.generateInternal = function(callback, cont) { this.forks = []; if ((this.moves.length === 0) && (Model.Game.design.failed !== true)) { var mx = null; for (var pos in this.pieces) { var piece = this.pieces[pos]; if ((piece.player === this.player) || (Model.Game.sharedPieces === true)) { for (var move in Model.Game.design.pieces[piece.type]) { if (move.type === 0) { var g = Model.Game.createGen(move.template, move.params); g.init(this, pos); this.addFork(g); if (Model.Game.design.modes.length > 0) { var ix = Model.find(Model.Game.design.modes, move.mode); if (ix >= 0) { if ((mx === null) || (ix < mx)) { mx = ix; } } } } } } } for (var tp in Model.Game.design.pieces) { for (var pos in Model.Game.design.positions) { for (var move in Model.Game.design.pieces[tp]) { if (move.type === 1) { var g = Model.Game.createGen(move.template, move.params); g.init(this, pos); g.piece = new ZrfPiece(tp, this.player); g.from = null; g.mode = move.mode; this.addFork(g); if (Model.Game.design.modes.length > 0) { var ix = Model.find(Model.Game.design.modes, move.mode); if (ix >= 0) { if ((mx === null) || (ix < mx)) { mx = ix; } } } } } } } while ((this.forks.length > 0) && (callback.checkContinue() === true)) { var f = this.forks.shift(); if ((mx === null) || (Model.Game.design.modes[mx] === f.mode)) { f.generate(); if ((cont === true) && (f.moveType === 0)) { CompleteMove(this, f); } } } if (cont === true) { Model.Game.CheckInvariants(this); Model.Game.PostActions(this); if (Model.Game.passTurn === 1) { this.moves.push(new ZrfMove()); } if (Model.Game.passTurn === 2) { if (this.moves.length === 0) { this.moves.push(new ZrfMove()); } } } } if (this.moves.length === 0) { this.player = 0; } return this.moves; } 

The algorithm is constructed in such a way that the continuations of moves “overwrite” their shorter “prefixes” (of course, if the " pass partial " option is not enabled).

Using these two methods (alignment of move generators into a “chain” and cloning), you can implement any constructions of the ZRF language. Of course, the implementation is not simple and, due to the need to ensure compatibility with the ZRF semantics, is rather confusing. This is not a big problem if the code works. The problem is that ZRF itself is far from perfect!

Open your fingers


This year began with disappointments. To begin with, I was stumped by my attempts to create a universal DSL suitable for the simple description of all the board games I know. Universally, in principle, it turned out, “understandable” - no. Even relatively simple games, such as Fanoron , were keen to write about in some kind of horror.

Like this
(*) [p] | ((\ 1 [ex]) *; ~ 1 (~ 1 [ex]) *)

Even on ZRF it looks clearer.
 (define approach-capture ( $1 (verify empty?) to $1 (verify enemy?) capture (while (enemy? $1) $1 capture) (add-partial capturing) )) (define withdrawl-capture ( $1 (verify empty?) to back (opposite $1) (verify enemy?) capture (while (enemy? (opposite $1)) (opposite $1) capture) (add-partial capturing) )) 

With Jocly, the matter also somehow didn’t immediately set. I did not like her architecture. Let's start with the fact that to store the state of the board it uses a mutable Model.Board singleton. How to work with this AI-bot - I'll never know. But the main thing is not even that. One model in it is completely different from the other (just has nothing to do). At the same time, “magical” members, like mWho or mMoves , are actively used , and the presentation should “know” how the model works , since it uses it on a par with the controller!

My hopes to “replace” the model were doomed to failure in advance! That is, it is quite possible for me and it will be possible to replace the model of " Turkish drafts"so that the appropriate presentation worked with her , but for any other game (even for English Drafts ) you would have to start everything from the beginning, because her model differs very significantly from Turkish Drafts. I realized that I was not ready, in addition to the model, engage in more and developing presentation and was in a deep depression. and then, the work involved jonic and on the horizon a little brightened.

We decided to give up trying to integrate with Jocly and elaborate missing controllers (for network and local games, as well as utility autoplay ), presenter Supports (2D and 3D), as well as bots (in stock) independently. Moreover, he agreed to do all this work jonicso I can focus on working on the model. First, I got rid of Jocly's stupid inherited restrictions. Yes, now the model supports games for more than two players! And then I got a taste ...

This is a list of options I have planned.
  • maximal-captures = true — ( « »)
  • pass-partial = true — ( «»)
  • pass-turn = true —
  • pass-turn = forced —
  • discard-cascades = true —
  • include-off-pieces = true —
  • recycle-captures = true —
  • smart-moves = true — «» UI ( )
  • smart-moves = from —
  • smart-moves = to —

  • zrf-advanced = true — zrf-advanced
  • zrf-advanced = simple —
  • zrf-advanced = fork — ZRF_FORK
  • zrf-advanced = composite —
  • zrf-advanced = mark — mark/back
  • zrf-advanced = delayed — « » ( , )
  • zrf-advanced = last — last-from last-to
  • zrf-advanced = shared — ( « »)
  • zrf-advanced = partial — ,
  • zrf-advanced = numeric — ( «»)
  • zrf-advanced = foreach — foreach
  • zrf-advanced = repeat — ( )
  • zrf-advanced = player — , ,
  • zrf-advanced = global — ( Axiom)

  • board-model = heap — ( )
  • board-model = stack — ( « »)
  • board-model = quantum — ( )

I told you that I don’t like the restrictions of ZRF either? A smaller part of these options are the inherited Zillions of Games settings, which must be supported. The rest is extensions that have not been seen in ZRF before. So all the zrf-advanced options (you can enable them all together, with one team) expand the ZRF semantics, making it more convenient (I tried to accommodate the wishes of Zillions of Games users ), and the board-model options introduce new types of boards.

This is worth saying more
, Zillions of Games . , . « » ( capture ). , . ( " " " "), «» , .


, «» , , . — , . 6 , , , Zillions of Games. , , () .


. . , ( ZRF , ), , . ( ), .


— . , ( ). , , , , .

The options themselves are implemented as loadable JavaScript modules. For example, if the game (as in “International checkers”) requires you to take the maximum number of pieces, you need to load the corresponding module after loading the zrf-model . Module connection is performed by the checkVersion function:

In zrf file
 ... (option "maximal captures" true) ... 

In javascript file
 ... design.checkVersion("z2j", "1"); design.checkVersion("zrf", "2.0"); design.checkVersion("maximal-captures", "true"); ... 

The model will check the compatibility of versions of the requested modules and enable the appropriate options. This expanding mechanism gave me an interesting thought. In some games, there are rules that are devilishly difficult to implement using only ZRF. In most cases, these rules are reduced to additional checks that affect the ability to perform a particular move. Putting checks into loadable options will relieve me of the need to extend the base language to implement them (which would not be easy at all).

It is worth noting that the developers Zillions of Games went the same way
, ( ) ZRF ( ), AI, Zillions of Games, . «» , DLL-. API AI, .

Axiom Development Kit — , , , ForthScript. Zillions of Games, . JavaScript, . , !

Let me explain by example.There is a kind of "Turkish drafts", which I learned about recently. The only difference between the “Bahraini checkers” and the Turkish ones is that they are not allowed to respond with an attack on an enemy attack. You can eat the attacked figure or get out from under the blow, but you can not attack another figure in return! Taking into account the fact that the rule also applies to ladies, the implementation of this game on ZRF turned out to be quite complex and, most importantly, not very “transparent”. But if I use extensible options, I have no need to complicate the code in ZRF!

Bahrain Dama
 (variant (title "Bahrain Dama") (option "bahrain dama extension" true) ) 

I can take the " Turkish checkers " and connect the option that performs the necessary checks. The loadable module replaces the method of post-processing a move and, if necessary, can prohibit a previously generated move! The verification logic itself can be arbitrarily complex, it will still be more understandable than a similar implementation on ZRF! The case is not limited to the additional validation of already generated moves. The option can "enrich" the course! For example, performing a move in “ Go ”, you must do the following:


All this can be “hidden” in a JavaScript extension ! It not only performs the necessary checks, but also completes the course by removing enemy stones. ZRF-description of the game becomes elementary ! Moreover, the extension is also suitable for other games! For example, for " Multicolor Go ".

More than one move ...


Expandable options allowed me to look at the project in a new way, but one small task still haunted me. In some games, under certain conditions, it is allowed to take any opponent's piece from the board . For example, in " Mill ":



This is not to say that this is unrealizable on ZRF, but the code is very complicated. Well, in general, generating a set of moves that are the same in almost everything except for the piece to be taken is a rather dull decision. I thought it would be much more convenient if you could use arrays of positions in the actions of the moves :

 ZrfMove.prototype.capturePiece = function(pos, part) { - this.actions.push([ pos, null, null, part]); + this.actions.push([ [pos], null, null, part]); } 

It was quite a global alteration of the code, but unit tests , once again, helped. So far, such non-deterministic moves are planned to be generated only from JavaScript extensions, as part of the “enrichment” of the moves generated by the simplest ZRF description of the game. If we talk about the " Mill ", then we are talking about all the same addition to the course of taking the pieces. Just instead of a set of single takes, one non-deterministic is added:

Magic of non-determinism
 Model.Game.CheckInvariants = function(board) { var design = Model.Game.design; var cnt = getPieceCount(board); for (var i in board.moves) { var m = board.moves[i]; var b = board.apply(m); for (var j in m.actions) { fp = m.actions[j][0]; tp = m.actions[j][1]; pn = m.actions[j][3]; if ((fp !== null) && (tp !== null)) { if (checkLine(b, tp[0], board.player) === true) { var all = []; var captured = []; var len = design.positions.length; for (var p = 0; p < len; p++) { var piece = b.getPiece(p); if (piece.player !== board.player) { if ((checkLine)(b, p, b.player) === false) { captured.push(p); } all.push(p); } } if (captured.length === 0) { captured = all; } if (captured.length > 0) { captured.push(null); m.actions.push([captured, null, null, pn]); } } ... break; } } } CheckInvariants(board); } 

But this is a broader concept. Not only taking can be non-deterministic! Remember that in the "Mill" there is a rule according to which the three remaining pieces of the player can jump "anywhere." In fact, this is a non-deterministic move to any free position:

Some more magic
  ... if (cnt === 3) { var len = design.positions.length; for (var p = 0; p < len; p++) { if (p !== tp[0]) { var piece = board.getPiece(p); if (piece === null) { tp.push(p); } } } } ... 

The moving figure can also be an array! According to the rules of transformation in Chess, a pawn, reaching the last rank, can turn into any of 4 pieces (Knight, Elephant, Rook, Queen), at the player’s choice. This is nothing more than a non-deterministic transformation performed when the figure is moved. In the ZRF code, the pawn can be turned into a queen, for example, and in a JavaScript extension :

... enrich this transformation
 var promote = function(arr, name, player) { var design = Model.Game.design; var t = design.getPieceType(name); if (t !== null) { arr.push(design.createPiece(t, player)); } } Model.Game.CheckInvariants = function(board) { var design = Model.Game.design; for (var i in board.moves) { var m = board.moves[i]; for (var j in m.actions) { fp = m.actions[j][0]; tp = m.actions[j][1]; if ((fp !== null) && (tp !== null)) { var piece = board.getPiece(fp[0]); if ((piece !== null) && (piece.getType() === "Pawn")) { var p = design.navigate(board.player, tp[0], design.getDirection("n")); if (p === null) { var promoted = []; promote(promoted, "Queen", board.player); promote(promoted, "Rook", board.player); promote(promoted, "Knight", board.player); promote(promoted, "Bishop", board.player); if (promoted.length > 0) { m.actions[j][2] = promoted; } } } break; } } } CheckInvariants(board); } 

For the controller changes a little. Having received a move from the model that is valid for the current state of the board, he must check the size of the arrays in each of the actions. If more than one element is transmitted, the controller must go through all possible options, forming deterministic moves. I think it is not necessary to say that one should be careful with such non-determinism. The Cartesian product of several independent positions is capable of generating an incredible number of different moves!

Subtotals


In general, I can say that I like the direction of the project development. I abandoned the idea of ​​creating something revolutionary new (although it was not easy) and focused on achievable, but not less interesting goals. It can be said that of the two famous birds, I prefer the “crane in hand”. Work on the project helps to master a new language for me, and joining a new, more experienced developer to the project carries the hope that the work will still be crowned with success. I refused the “revolution”, but the project continues to evolve!

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


All Articles