📜 ⬆️ ⬇️

Dagaz: Looking for talents

image Do with us
do as we do
do better than us!

80's telecast

I must admit, I am not very good at developing bots. I am sure there are people who can do this much better than me. And I would really like such people to join the project . In terms of material incentives, I can offer little. Dagaz was conceived as a free and open-source alternative to Zillions of Games . I myself do not mind its commercial use, just have not yet figured out how to do it.
')
The project will certainly continue to evolve and remain free (at its core). Any person who has contributed to its development, I will consider my friend and co-author. Of course, attribution, for all modules in the development of which these people took part, will be mandatory. In addition, I am ready to provide any feasible technical assistance for the project. For example, I can talk about how to develop game bots.

First of all, it is worth noting that far from all games the issue of developing bots is acute. In the case of puzzles, a bot who can find a solution is more a luxury than a vital necessity. In addition, there are games, for objective reasons, much more difficult for a person than for a computer. This fully applies to various mankalam .


Winning at the computer in such a game is hard. And this is not because you can not play it! I know a man who can . So, he won far from the first attempt and with a very small margin. And this is despite the fact that it uses far from the most successful bot , which is much more modest in other games . Mancala seemed to be specially created for computers! But it's not just them.


Here the bot is even simpler , but the game itself is asymmetric. The task of the “fox” (there is everything that is possible) is much simpler than the task of the “geese” (to hold down the “fox”, depriving it of its course) and a computer plays for the “fox”. If someone will be able to win it, inform. Because I have not succeeded yet. The process itself seems to be understandable, but you constantly lose sight of some “trifle”.


Also, there are games that bots do quite well, although they could play much better. Reversi is a great example. I am sure that it will not be difficult for a person who plays this game well to cope with a bot, but for me, for example, this is an impossible task. Alas, my bots cannot cope with most of the most popular games at all. Here is a list of those for which this problem is most relevant:

  1. Checkers - first of all " Russian ", " International " and " Frisian ". Bots with them, in principle, work, but they play at the level of a very weak beginner. Also very interesting are the " Turkish checkers ", here the bots play even worse. The closest attention should be paid to the “ Pillars ” and “ Weasel ”. These are really challenging and very interesting games!
  2. Chess and everything like them. In these games, all my bots are playing absolutely disgusting. The same applies to the " Chinese " and, to an even greater extent, the " Japanese " chess. Here, I'm not talking about the " big games ", the development of bots for which is quite specific and in which few people play (I know such people). First of all, the most popular options are interesting for me. Shashmatas are also a priority. They generally should be put in a separate category, since they took the most difficult "of both worlds." First of all, I am interested in " Belarusian chess " and " Altai tent ". Fans of very hardcore exotics can try their hand at Platform Chess .
  3. Staged games, above all " Renju " and " Go ". This is a very fresh topic, in the sense that I have just begun them. In general, games with resetting pieces to the board (on a par with mancala) are the theme of the current iteration of the project, so there will be more such games soon. And now it is clear that some of them (Guo for example) require a special approach when developing AI. But there are even more confused games .
  4. The most diverse games fall into this category. Here are the children’s “ Jungle ” and the French “ Agon ” of the 18th century. Games are big and smaller . The games are completely insane . One thing unites them all. I have no idea how to approach the development of their AI. And, of course, I will be just happy if someone helps bring to mind the AI ​​for my " Spock ".

So how can you write your bot? As a very first step, it is worth downloading the contents of this or this repository to your computer (without which to write, and even less to debug, which will be hard). Although the second of the listed repositories is, in fact, a release, it does not currently use any obfuscation or minification. All applications are very simple. From a html-file several js-scripts are loaded sequentially.

Like this
<!DOCTYPE html> <html> <head> <META http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>Turkish Dama</title> </head> <body> <img id="Board" style="display:none" src="images/turkish.png"> <img id="WhiteMan" style="display:none" src="images/wman.png"> <img id="BlackMan" style="display:none" src="images/bman.png"> <img id="WhiteKing" style="display:none" src="images/wdamone.png"> <img id="BlackKing" style="display:none" src="images/bdamone.png"> <table id="Table" style="margin:auto; font-family:sans-serif; font-size:14px"> <tr><td>Traditional - Turkish <a href="turkish-dama-board.htm">no AI</a></td></tr> <tr style="height:548px; vertical-align:top"> <td id="CanvasCell" style="width:548px"> <canvas id="Canvas" width="548" height="548" style="cursor:default">Broken canvas...</canvas> </td> <td style="width:300px;"> <div id="ScrollDiv" style="height:510px; overflow:auto"> <img id="GlyphImage" /> <p id="HelpText"></p> <p id="GameSession"></p> </div> </td> </tr> <tr> <td> <div style="height:100px; width:500px; margin-left:auto; margin-right:auto"> <table> <tr id="PieceInfo" style="display:none"> <td> <img id="PieceInfoImage" /> </td> <td id="PieceInfoText"></td> </tr> </table> </div> </td> </tr> </table> <script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script> <script src="../common-scripts/dagaz.js"></script> <script src="../common-scripts/zrf-model.js"></script> <script src="../common-scripts/zobrist.js"></script> <script src="../common-scripts/common-setup.js"></script> <script src="../common-scripts/2d-view-v2.js"></script> <script src="../common-scripts/move-list-v2.js"></script> <script src="../common-scripts/maxmin-ai-v2.js"></script> <script src="../common-scripts/sgf-parser.js"></script> <script src="data/turkish.js"></script> <script src="../common-scripts/sgf-ai.js"></script> <script src="scripts/maximal-captures.js"></script> <script src="scripts/turkish-dama.js"></script> <script src="../common-scripts/app-v2.js"></script> <script src="../common-scripts/analytics.js"></script> </body> </html> 

You may notice that from among third-party libraries, I use only very convenient Underscore for me (I do not insist on using it). All bot scripts contain "-ai" in their name. It is they who are interesting to us now. But before you move on, you should tell a little about:

What is game design?
First of all, Dagaz separates the design of the game (ZrfDesign) from its state (ZrfBoard). The design defines the topology of the board and the rules for moving the pieces, and the state determines which pieces are and where they are at this particular moment. ZrfDesign object is created once for the whole game, ZrfBoard objects can be created (including, for the needs of bots) an unlimited number. At the beginning of the game, the design is initialized by calling the Dagaz.Model.BuildDesign function:

turkish-dama.js
 Dagaz.Model.BuildDesign = function(design) { design.checkVersion("z2j", "2"); design.checkVersion("zrf", "2.0"); design.checkVersion("animate-captures", "false"); design.checkVersion("smart-moves", "true"); design.checkVersion("maximal-captures", "true"); design.addDirection("w"); design.addDirection("e"); design.addDirection("s"); design.addDirection("n"); design.addPlayer("White", [1, 0, 3, 2]); design.addPlayer("Black", [0, 1, 3, 2]); design.addPosition("a8", [0, 1, 8, 0]); design.addPosition("b8", [-1, 1, 8, 0]); ... design.addPosition("h1", [-1, 0, 0, -8]); design.addZone("promotion", 1, [0, 1, 2, 3, 4, 5, 6, 7]); design.addZone("promotion", 2, [56, 57, 58, 59, 60, 61, 62, 63]); design.addCommand(0, ZRF.FUNCTION, 24); // from design.addCommand(0, ZRF.PARAM, 0); // $1 design.addCommand(0, ZRF.FUNCTION, 22); // navigate design.addCommand(0, ZRF.FUNCTION, 2); // enemy? design.addCommand(0, ZRF.FUNCTION, 20); // verify design.addCommand(0, ZRF.FUNCTION, 26); // capture design.addCommand(0, ZRF.PARAM, 1); // $2 design.addCommand(0, ZRF.FUNCTION, 22); // navigate design.addCommand(0, ZRF.FUNCTION, 1); // empty? design.addCommand(0, ZRF.FUNCTION, 20); // verify design.addCommand(0, ZRF.IN_ZONE, 0); // promotion design.addCommand(0, ZRF.IF, 4); design.addCommand(0, ZRF.MODE, 0); // jump-type design.addCommand(0, ZRF.FUNCTION, 25); // to design.addCommand(0, ZRF.JUMP, 3); design.addCommand(0, ZRF.PROMOTE, 1); // King design.addCommand(0, ZRF.FUNCTION, 25); // to design.addCommand(0, ZRF.FUNCTION, 28); // end ... design.addPriority(0); // jump-type design.addPriority(1); // normal-type design.addPiece("Man", 0, 1); 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); ... design.setup("White", "Man", 48); design.setup("White", "Man", 49); ... } 

All this code is generated from a zrf file automatically and you don’t need to touch it. What is worth paying attention to? The addPlayer calls define players and, in the simplest case, the order of their moves (we will not be distracted by the second argument of the function). The addPosition function defines a single position on the board, and addDirection defines a direction. At the very end, the setup calls define the initial placement of figures.

Positions and directions are simply two linear arrays, indexed from scratch. Thus, the position of a piece on the board is always determined by a non-negative integer number. Such a numerical value, at any time, can be turned into a position name using the Dagaz.Model.posToString function. The inverse transform is performed by Dagaz.Model.stringToPos .

With directions a little harder. As well as positions, they are encoded with non-negative integers (according to the order of the addDirection call when configuring the game design). You can get the direction code from the name using the design object's getDirection method. The allDirections method allows you to get an array of all available directions (there is a similar allPositions method for obtaining a list of all positions). But what is the direction itself?

Remember the second argument to addPosition ? In fact, this is an array of offsets within a linear list of all positions, one for each direction. If the corresponding value is zero, moving in the specified direction from this position is prohibited (there is no such direction). Navigation itself is carried out by the following method:

 ZrfDesign.prototype.navigate = function(player, pos, dir) { if (!_.isUndefined(this.players[player])) { dir = this.players[player][dir]; } if (this.positions[pos][dir] != 0) { return + pos + this.positions[pos][dir]; } else { return null; } } 

As you can see, simply add an offset to the position index or return null if such a move is prohibited. All movements must be formulated in the first player’s “coordinate system”. This means that, for example, in Chess, black pawns, as well as white, move "north". The correct argument of the directions is the second argument of the addPlayer method.

It remains to talk about gaming areas. These are simply lists of “special” positions on the board, determined by the addZone method. Zones are individual for each player. This means that the same (by name) zone, for different players, may contain different positions. To check whether a position is in a given zone, the inZone method is used . All definitions of the listed functions can be found in the source code of the zrf-model.js module.

Thus, we have a design that does not change during the game, and a set of game states encoding the arrangement of figures at the time of the start of each turn, but a legitimate question arises:

How to change the game state?
Generally speaking, no way, because you should never change it yourself! Of course, the zrf-model has a setPiece method that allows you to place a piece on the board, but you don’t need to call it directly! The game state is immune. The new state is created from the old by applying a stroke to it (an object of type ZrfMove), using the apply method.

Where do these very moves come from? They are generated by the game state itself when calling the generate method. At the same time, the design and expansion of the game ensure that only the moves allowed by the rules of the game are included in the list of formed moves! As well as the game state, the moves should not be changed directly by the bot developer, but you can at any time generate admissible moves, get a new game state using the move and, of course, read any information contained in these objects.

So how can ZrfBoard please us? First of all, we can always find out whose move is now by turning to the member player (in contrast to the positions and directions, the players in Dagaz are indexed starting from one). The integer zSign member contains a zobrist-hash value that identifies an array of shapes. The getPiece method allows you to get a figure located at a specified position (a numeric index is accepted). If the method returns null, then the polled position is empty.

The figure itself (ZrfPiece) contains two members: type - the type of the figure (the numerical value is indexable from zero) and the owner, the player . In addition, ZrfPiece objects can contain attribute values ​​(indexed by numeric keys). The getValue method allows you to get this value by specifying an index. As usual, null means the absence of the desired value. It is allowed to store values ​​of both string and numeric types.

It remains to deal with the moves (ZrfMove). This is the hardest part. In many games, such as Chess and Checkers, “complex” moves are allowed, including the simultaneous movement of several pieces, the successive taking of pieces, one after the other, etc. Since a move translates the game from one consistent state to another, all these actions are encoded in one move and are applied atomically (all or nothing).

The sequence of all actions performed is contained in the actions array. Each elementary action is a four-element array containing the following elements:

0 - Array of starting positions of movement
1 - Array of end positions of displacement
2 - Array of figures (objects of type ZrfPiece) placed on the board
3 - An integer containing the partial stroke number within the composite

Each of the listed arrays usually consists of one element. Larger arrays are rarely used for non-deterministic moves used in some games. The number of the partial move determines the sequence of actions. If for two actions this value is the same, they are executed simultaneously. Otherwise, an action with a lower partial stroke number is performed first. Three types of actions are used:

  1. Take a shape - the zero item is filled, the first item contains null
  2. Reset (adding a shape to the board) - the null element is null, the first and second elements are filled
  3. Moving - zero and first elements are filled, the second element can also be filled (if the figure turns in the process of making a turn)

The toString method allows you to get a textual description (notation) of the move. Sometimes it helps a lot in the debugging process.

The whole game is, in essence, a repeating cycle, consisting of the generation of permissible moves and their subsequent application to the game state. The iterations are repeated (with the alternation of moves of the players, given by the rules of the game) until one of the following events occurs.

  1. The list of moves generated by one of the players turns out to be empty (depending on the game, this can be regarded as a defeat, a draw or even a player's victory)
  2. The Dagaz.Model.checkGoals function returns a non-null value.

The second point allows you to program the correct completion for almost any game.

Here, for example, how it looks for Reversi
 var isValid = function(design, board, player, pos) { for (var dir = 0; dir < design.dirs.length; dir++) { var p = design.navigate(player, pos, dir); if (p === null) continue; var piece = board.getPiece(p); if ((piece === null) || (piece.player == player)) continue; while (p !== null) { p = design.navigate(player, p, dir); if (p !== null) { piece = board.getPiece(p); if (piece === null) break; if (piece.player == player) return true; } } } return false; } var checkGoals = Dagaz.Model.checkGoals; Dagaz.Model.checkGoals = function(design, board, player) { var fc = 0; var ec = 0; var positions = []; _.each(design.allPositions(), function(pos) { var piece = board.getPiece(pos); if (piece === null) { positions.push(pos); } else { if (piece.player == player) { fc++; } else { ec++; } } }); var f = true; _.each(positions, function(pos) { if (f) { if (isValid(design, board, player, pos)) f = false; if (isValid(design, board, design.nextPlayer(player), pos)) f = false; } }); if (f) { if (fc == ec) return 0; if (fc > ec) { return 1; } else { return -1; } } return checkGoals(design, board, player); } 

Pay attention to the isValid function. With it, the presence of moves allowed by the rules of the game is determined, and only if no player can make such a move is the winner determined (just the player who has more pieces on the board).

The checkGoals function is an integral part of the game design. At the time of the development of the bot, it is already defined and it is quite enough just to be able to call it, to correctly determine the positions that complete the game. The function can be called for any game state and takes three arguments: the design of the game, the current state of the game and the player's identifier from the point of view of which the position is evaluated. The result of the call is interpreted as follows:

1 - player win
-1 - player loss
0 - draw
null - not a terminal position (the game can be continued)

Another important thing that you need to know before you start developing any bots is an evaluation function. I have already written about it in some detail before, but I repeat once again. The objective of the evaluation function is the numerical evaluation of the position, from the point of view of one of the players (the greater the value, the better the position).

Here's what she might look like.
 Dagaz.AI.eval = function(design, params, board, player) { var r = 0; _.each(design.allPositions(), function(pos) { var piece = board.getPiece(pos); if (piece !== null) { var v = design.price[piece.type]; if (piece.player != player) { v = -v; } r += v; } }); return r; } 

Usually (but not necessarily) the assessment of the position, from the point of view of one of the players, coincides with the assessment of the same position by his opponent, taken with the opposite sign. Without compliance with this condition will not work, for example, minimax algorithms . There are games (for example, those in which more than two players participate) in which it is not possible to comply with this condition. Also, there are games in which the use of the minimax is difficult, for other reasons (for example, a complex sequence of passing a turn).


In these cases, heuristics come to the fore (when using a minimax, they are also very useful, since they allow you to pre-sort possible moves by their potential "strength"). Unlike the evaluation function, heuristics evaluate the move itself, and not the position on the board after it has been completed. Heuristics can be both very complex and as simple as possible.

Such, for example
 Dagaz.AI.heuristic = function(ai, design, board, move) { return move.actions.length; } 

In the Tibetan game " Ming-Mang ", the more we take the enemy pieces per turn, the better. Since the move in Ming-Mang consists of moving one's figure and an arbitrary number of takes, in order to get a good heuristic, it is enough just to estimate the number of elements in the move.actions array (I told about the structure of the move in Dagaz above).

Not for all games to build a qualitative evaluation function is easy. In particularly difficult cases, “little tricks” are good at helping. About the "cover", very useful in chess-type games, I wrote earlier . Another possibility is to store auxiliary values ​​in the attributes of the shapes.


In Rendzu , the most important aspect is the presence of “triples” and “fours” on the board, oriented in four different directions. This information is used not only by AI, but also to identify situations of various “fouls” (long rows and “forks”) prohibited for the first player, as well as to determine the end of the game (5 stones “in a row”). Meanwhile, the search for such configurations on the board is quite a resource-intensive task.

On the other hand, if we already store counters with row lengths, for all stones previously placed on the board, the task of updating these counters, when adding a new stone, becomes trivial . Slightly more complicated (but quite realizable ) is the calculation of the number of " dame " for groups of stones in the games of the " Go " family. In any case, it is much easier to store these values ​​than to calculate again and again, whenever they are needed. Of course, these little things, by themselves, will not turn a bot into AlphaGo , but can make its development much more comfortable.

So we come to talk about bots. The task of the bot, in Dagaz is the selection of the best (in a sense) move from the list of all allowable moves generated by the game state. Here is the simplest bot:

random-ai.js
 (function() { function RandomAi(params) { this.params = params; if (_.isUndefined(this.params.rand)) { this.params.rand = _.random; } } var findBot = Dagaz.AI.findBot; Dagaz.AI.findBot = function(type, params, parent) { if ((type == "random") || (type == "solver")) { return new RandomAi(params); } else { return findBot(type, params, parent); } } RandomAi.prototype.setContext = function(ctx, board) { ctx.board = board; } RandomAi.prototype.getMove = function(ctx) { var moves = Dagaz.AI.generate(ctx, ctx.board); if (moves.length == 0) { return { done: true, ai: "nothing" }; } if (moves.length == 1) { return { done: true, move: moves[0], ai: "once" }; } var ix = this.params.rand(0, moves.length - 1); return { done: true, move: moves[ix], ai: "random" }; } })(); 

Yes, yes, just the choice of a random move from all available. By the way, do not think that this is a completely useless bot. He has his own very important applications.

Already on this listing you can understand the following:

  1. Any bot must “integrate” into the Dagaz.AI.findBot call chain (so that the controller can find it. The bot is searched by its type. There are three main types: “opening” , “common” and “random” . Found bots also lining up “in a chain.” This is done so that the best move first looks for a bot destined for debuts ( sgf-ai , for example), followed by the main bot ( maxmin-ai , in most cases), and if it is not found (found it all moves too bad), the game turned on random-ai , just to go at least as a something. For the decision of the head Omoko uses a special type of bot "solver".
  2. For each player with whom the bot works, a “context” is created (this is done so that one bot can serve several different players simultaneously). Inside the context, you can store any data shared between successive calls to the bot (for example, a tree of game states). Before requesting the “best” move from the bot, the controller sends the bot the current state of the game (by calling the setContext method).
  3. The bot's move is requested by the getMove method. The method returns a hash containing the move field (the desired move). Also, if the bot has completed the search for a move, it must pass true to the done field of the returned hash. Currently, this functionality is not used, but it is understood that the controller may request the bot to move several times in a row. If the bot did not manage to find the best move in the allotted time, it can return the best move from those already reviewed, but not set the done field for the controller to continue polling.

Let's look at a slightly more complicated bot:

heuristic-ai.js
 (function() { Dagaz.AI.NOISE_FACTOR = 10; function Ai(params, parent) { this.params = params; this.parent = parent; if (_.isUndefined(this.params.NOISE_FACTOR)) { this.params.NOISE_FACTOR = Dagaz.AI.NOISE_FACTOR; } } var findBot = Dagaz.AI.findBot; Dagaz.AI.findBot = function(type, params, parent) { if ((type == "heuristic") || (type == "common") || (type == "1") || (type == "2")) { return new Ai(params, parent); } else { return findBot(type, params, parent); } } Ai.prototype.setContext = function(ctx, board) { if (this.parent) { this.parent.setContext(ctx, board); } ctx.board = board; ctx.timestamp = Date.now(); } Ai.prototype.getMove = function(ctx) { ctx.board.moves = Dagaz.AI.generate(ctx, ctx.board); if (ctx.board.moves.length == 0) { return { done: true, ai: "nothing" }; } var nodes = _.chain(ctx.board.moves) .map(function(m) { return { move: m, weight: Dagaz.AI.heuristic(this, ctx.design, ctx.board, m) }; }, this) .filter(function(n) { return n.weight >= 0; }).value(); if (this.params.NOISE_FACTOR > 1) { _.each(nodes, function(n) { n.weight *= this.params.NOISE_FACTOR; n.weight += _.random(0, this.params.NOISE_FACTOR - 1); }, this); } if (nodes.length > 0) { nodes = _.sortBy(nodes, function(n) { return -n.weight; }); return { done: true, move: nodes[0].move, time: Date.now() - ctx.timestamp, ai: "heuristic" }; } if (this.parent) { return this.parent.getMove(ctx); } } })(); 

This is not so much "artificial intelligence" as "artificial instincts." Just choosing the course with the maximum heuristics, without any use of the evaluation function. This bot is fast and transparent in debugging, but this is where its merits end. The depth of his intellect, you can appreciate the current level of the game " Renju ", " Wars of viruses " and especially " Atari Go ". Ideally, I would like to achieve normal operation of something like this (my implementation of a minimax with alpha-beta clipping is, alas, not working yet).

Speaking of debugging bots, it is worth noting that this is a painstaking, tedious thing and does not have much fun. Any tool to facilitate this process is definitely worth using. First of all it concerns the common-setup module. It is enough to add it to the list of loaded scripts and in the log (how to enable the log in the browser, I think it is not necessary to explain), after each turn, records like this will start to appear:

 Setup: ?setup=+19;0:1=4;+2;0:1=4;+5;0:2=3;+7;0:1=2;0:2=5;+7;0:2=5;+29; 

By adding this entry to the address bar of the browser (like this ), you can recreate the corresponding position immediately, without tedious playing of the previous moves.And with some skill, you can make yourself ready to read this notation and even edit it directly in the address bar.

Another useful feature is provided by the controller. If you replace app-v2 with app-auto in the htm-file , you can make bots fight each other without being distracted by clicking on the board with the mouse. Remember the weird numeric bots in heuristic-ai.js ?

 ... Dagaz.AI.findBot = function(type, params, parent) { if ((type == "heuristic") || (type == "common") || (type == "1") || (type == "2")) { return new Ai(params, parent); } else { return findBot(type, params, parent); } } ... 

A bot with type " 1 " will be loaded for the first player, and with type " 2 " for the second. This is certainly not an autoplay from the Axiom , but also a very useful thing. In addition, unlike autoplay , app-auto is not limited to only two players .

The debug-ai module is another useful tool worth adopting. Sometimes, when debugging game logic, it is not enough just to reproduce the position. In such cases, it is sometimes necessary that the bot play the moves on a given list and debug-ai knows how to do it. It was with his help that I debugged the Apocalypse .

Well, in general, and all wisdom. As I said, the tedious thing, but not supernatural. And I will be glad if anyone joins me in this matter. By the way, literally two days ago, the first letter arrived at the project mail. Garrick Wells great help to me with debuts for Micro Shogi . Along the way, I discovered and fixed a serious bug that affected this and several other games. This is exactly the form of cooperation I seek.

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


All Articles