📜 ⬆️ ⬇️

Dagaz: Architecture

image All this is so architecture
I will cure you of your illness
Trust me as a doctor
My medicine will help you.

Tristan Tips - " The Dog in the Hay "

In my previous article I talked a lot about how the Dagaz turn generator works . Perhaps I put the cart before the horse. My most detailed description does not help at all to understand the main thing - how all this can be used. In fact, it is simple.

There is simply no place


In order to use the move generator, it is not necessary to thoroughly understand how it works. It is important to understand the purpose of only three classes. And the first one is ZrfBoard . As it should be clear from the name, this class describes the board.
')
I want to be understood correctly
I'm not talking about the size or shape of the board. All information about the allowable positions (fields of the board), as well as the relationships between them (directions) is stored in an instance of the ZrfDesign class and does not change during the game! I wrote a lot about this in previous articles and do not want to repeat. Game design is not the subject of this article. We can consider it as a “black box”.

ZrfBoard contains a description of the state of the game, at the start of a certain move. For the most part, this is information about the placement of pieces on the board. To change it, just two methods are enough:


All positions are simply integer values ​​(index of a large linear array). To convert to a more familiar string representation (and vice versa), the global functions of Model.Game.posToString and Model.Game.stringToPos are used . The description of the figure ( ZrfPiece ) is a little more complicated:

Constructor
function ZrfPiece(type, player) { this.type = type; this.player = player; } 

The type of the figure and its owner (both of which are integer values). It is important to understand that these are immutable values. The same ZrfPiece instance can be used on several board positions at the same time and even on positions of different ZrfBoard instances describing the state of the game at different times.

But what if you need to change the shape?
For example, a chess pawn , having reached the last rank, can turn into one of four pieces, and in such games as Reversi , the owner of the piece can change. The solution is obvious - any method that changes the “state” of a shape, in fact, simply returns a new instance of the class, leaving the old object unchanged:

Figure transformation
 ZrfPiece.prototype.promote = function(type) { return new ZrfPiece(type, this.player); } 

It may seem that I pay too much attention to this, but in fact, this is important. The fact is that in addition to information about the type and owner, the ZrfPiece class can contain additional numeric values ​​- attributes of the figures. The simplest example of using attributes is castling in Chess). The sign that the figure has moved before and cannot be used in the castling, can be saved in one of its attributes. Changing the value of any attribute also generates a new instance of the ZrfPiece , without changing the existing one.

There are two more methods you need to be aware of in order to use ZrfBoard :


Although we can change the state of ZrfBoard using the setPiece method, we should not do this directly. Instead, we have to select a move (one from the list of all possible) and apply it to the state of the board using the apply method, which, as is the case with the ZrfPiece change, will return a new instance of the object.

Of course, the moves are tied to the player.
Each copy of ZrfBoard is tied to the owner - the player performing the next move. In addition to moving figures, the apply method switches the player as well. This is not necessarily a simple alternation of two players. The times of the silly restrictions of Jocly are gone.

That's all you need to know to use ZrfBoard . The design of the game and the associated algorithms for generating moves can be very complex, but this does not change anything. We form all the available moves with the generate method and apply the selected move with the apply method (receiving a new state). The initial state of the board can be obtained using the global function Model.Game.getInitBoard .

What would we do without them


Turns are what translates one game state into another. The problem is that the moves are not always as simple as they may seem. In Shatranj (the immediate predecessor of Chess ), for a complete description of any move, it is sufficient to specify the initial and final position. Even taking and turning is not necessary to describe. A pawn always turns into a queen , and a capture is always “chess”. But already in Chess itself everything is not so simple!



A pawn appears that is able to beat a piece on the wrong field . Appears " castling " - a move, during which two figures are moved at once! It becomes clear that the course should consist of several actions. What are these actions? I can list three types:


I want to note that all these difficulties are not only in order to implement "castling" and "taking on the aisle." These rules are a touchstone, which allowed to expand the functionality of a universal solution in time. The same “cascading” moves that the Ziilions of Games team has introduced into their product in order to play castling in chess can be used with great success in many other completely different games. For example, in this:


Cascading move is not necessarily castling! In fact, this is any move that, when executed, moves several pieces at once. Such "non-standard" rules greatly enrich the product! And if Chess gave us cascade moves, then Checkers also found something to learn.



The composite move is performed “piece by piece” and this is important because, often, the player can choose several different continuations of the composite move performed by him. A move is not completed until all its partial moves have been completed, but a convenient user interface should provide the ability to perform each partial move separately! On the other hand, it is more convenient for AI bots to consider the composite move as a whole, as a single entity changing the game state. This is really a difficult problem, which I will discuss below.

What else do you need to know about the ZrfMove class? Only two methods:


From the previous section, we remember that we can apply a move to an instance of the ZrfBoard class to change the position on the board. The changeView method does the same thing, but with respect to the external, for the game model, visual presentation. The view receives simple commands from the model:


This is almost the same as the actions added to the course, with the exception that instead of numeric values ​​used by the model, the lines describing the positions and figures in the well-known representation of textual notation are transmitted. As for the toString method, it’s just getting the notation of a move in a form understandable to humans. A non-zero part value allows you to get a description of the corresponding partial move. By passing in argument 0, you can get a full description of the compound move.

Justified complexity


So, at each stage of the game we have a game state and a list of moves (allowed by the rules of the game) to choose from. This is quite enough for the application to work correctly, but from the point of view of the implementation of the user interface, the list of moves is not the most convenient thing. Let's look at how the Zillions of Games user interface works again:


First of all, the user selects one of his figures (indicating the field on which it is located). Further, if there are several embodiments of a partial move, the target positions are marked and the user can drag a figure onto one of them. If there is only one possible move, it is executed immediately (the “smart moves” option works). It's comfortable. This is much more convenient than offering a choice from the following list of moves:

  1. d8-g8-g3-d3-d7-h7-h5-a5-a7-e7-e1-c1-c6-a6-a1
  2. d8-g8-g3-d3-d7-h7-h5-a5-a7-e7-e1-a1-a6-c6-c1
  3. d8-h8-h3-d3-d7-g7-g5-a5-a7-e7-e1-c1-c6-a6-a1
  4. d8-h8-h3-d3-d7-g7-g5-a5-a7-e7-e1-a1-a6-c6-c2
  5. ...

Generally speaking, this is work for the controller , but there are too many specific logic already implemented in the model . It is not necessary for the controller to be aware that moves are divided into moving figures (possibly several figures at once) and adding them to the board. The controller should not be concerned about the correct order of actions when performing each partial move. The “smart moves” option should also not worry him. All this is implemented in the model!


Here are the two most important methods of the new class, designed to ensure mutual understanding of the model and controller. Instead of a “flat” array of moves, we get from ZrfBoard an instance of the class ZrfMoveList containing this list. The controller calls the getPositions method to get an array of positions available to perform the next step. One of these positions is selected using the user interface and passed to the setPosition method.

This method returns the textual notation of a partial move to the controller (for displaying in the list of moves), makes the necessary changes to the visual representation of the board, and prepares ZrfMoveList for the next step. The loop repeats until the next getPositions call returns an empty list. This means that we have reached the end of the composite stroke. Most often, the selected sequence of steps correspond to only one possible move from the entire list. Most often, but not always!


There are four possible moves with matching starting and ending positions of movement. For us, this means that after the getPositions method returns an empty list, the ZrfMoveList will contain more than one valid move. The controller must provide the user with a choice from this list. The list of allowable moves can be obtained by calling the getMoves method (as new values ​​are passed to setPosition , this list will decrease). There are several other methods worth mentioning:


If with the first two methods everything is more or less clear, then the following two require clarification. There are games (checkers do not apply to them) in which a player has the right to interrupt the execution of a composite move. For example, in Fanoron , although the first take is mandatory (as in checkers), the player can break the chain of take at any time:


Although the player can continue taking the “ D4-C5 ” move, he has the right to refuse this opportunity by pressing the “ Pass ” button. A special case of this situation is the player’s refusal to complete the entire composite move (in some games, for example, in Go , this is allowed). Before obtaining a list of positions, the controller must call the canPass method, to determine the validity of the early completion of a composite stroke. Calling the pass method will complete the execution of the move (if allowed by the rules). After that, the controller will need to get a list of valid moves, select one of them and apply it to the ZrfBoard to get a new state.


I told far from all the features of the ZrfMoveList class. In some games (for example, in Mill , shown above), the logic of a turn may be much more complex. It can include multiple movements of shapes, non-deterministic captures and dumping of shapes, and even non-deterministic movements! For us it is important that all these difficulties are safely hidden behind a simple and clear interface of the class ZrfMoveList . Yes, this class itself is very complex, but this complexity is justified! After all, had it not been for us, we would have to further complicate the controller.

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


All Articles