Tic Tac Toe «Sans FrontiÚres»

Tic-tac-toe ... tout le monde les a joués, j'en suis sûr. Le jeu est séduisant par sa simplicité, surtout lorsque vous faites glisser l'horloge quelque part dans la leçon, un couple, et il n'y a rien à portée de main, sauf une feuille de cahier et un simple crayon. Je ne sais pas qui a été le premier à avoir pensé à dessiner des croix et des cercles sur 9 cases, mais depuis lors, le jeu n'a plus perdu en demande, d'autant plus que les gens ont proposé de nombreuses variantes.



Cet article concerne le processus de développement de l'IA sur javascript pour jouer à l'une de ces variations de tic-tac-toe: j'ai beaucoup de matériel, mais je l'ai dilué avec de l'animation et des images. Dans tous les cas, au moins ça vaut le coup d'essayer de le jouer.
Les différences entre cette version du jeu et l'original sont les suivantes:

  1. Le champ peut ĂȘtre arbitrairement grand (combien de temps durera le bloc-notes)
  2. Le gagnant est celui qui met 5 piÚces (si vous pouvez les appeler ainsi) dans une rangée.

Tout est simple ... et en mĂȘme temps compliquĂ©: le rĂ©sultat du jeu ne peut pas ĂȘtre calculĂ© Ă  l'avance, comme dans l'analogue classique. Cette "petite projection" m'a enlevĂ© beaucoup de temps et de nerfs. J'espĂšre que vous le trouverez intĂ©ressant.

Avant de commencer


Forcé de m'excuser à l'avance pour le volume de l'article et à certains endroits une présentation de pensée pas tout à fait intelligible, cependant, je n'ai pas pu serrer le troupeau sans perte de contenu et de qualité.
Je vous recommande de vous familiariser d'abord avec le résultat . Code

Raccourcis clavier et commandes:

  • D - AI fera un pas pour vous
  • T - voir le poids des cellules
  • Écrivez SHOW_WEIGHTS = true dans la console pour afficher les poids de toutes les cellules analysĂ©es.

Commençons


Vous devez commencer par la mise en Ɠuvre du jeu lui-mĂȘme, c'est-Ă -dire Ă©crire une application pour deux joueurs, jusqu'ici sans bot. Pour mes besoins, j'ai dĂ©cidĂ© d'utiliser javascript + jquery + bootstrap4, bien qu'il n'y soit pratiquement pas utilisĂ©, mais il vaut mieux le laisser - sinon la table flottera. Il n'y a rien de spĂ©cial Ă  dire, il y a beaucoup de matĂ©riel sur js, jquery et bootstrap. Je peux seulement dire que j'ai utilisĂ© MVC. Quoi qu'il en soit, je n'expliquerai pas absolument tout le code - il y a dĂ©jĂ  eu beaucoup de matĂ©riel.

Le terrain de jeu Ă©tait donc prĂȘt. Vous pouvez dĂ©finir des formes dans les cellules. Mais la victoire de l'un des joueurs n'a Ă©tĂ© fixĂ©e en aucune façon.

Analyse de fin de partie


Le jeu se termine lorsque l'un des joueurs met 5 piÚces d'affilée. "C'est simple!" Pensai-je. Et il a commencé à balayer absolument toutes les cellules du champ: tout d'abord l'horizontale, puis la verticale, et enfin les diagonales.

C'est une façon stupide, mais cela a fonctionnĂ©. Cependant, cela pourrait ĂȘtre considĂ©rablement amĂ©liorĂ©, ce que j'ai fait: la plupart des cellules resteront vides tout au long du jeu - le terrain de jeu est trop grand pour ĂȘtre entiĂšrement rempli. Puisqu'il Ă©tait nĂ©cessaire de le scanner Ă  chaque mouvement, et qu'une seule piĂšce est placĂ©e en un seul mouvement - vous pouvez vous concentrer uniquement sur cette piĂšce (cellule): scannez seulement une horizontale, verticale et deux diagonales de la cellule qui possĂšde la mĂȘme cellule.

De plus, vous n'avez pas besoin de scanner toutes les lignées cellulaires. Puisque la fin du jeu est de 5 piÚces d'affilée, les piÚces qui sont à 6 cases les unes des autres ne nous intéressent pas. Il suffit de scanner cinq cellules de chaque cÎté. Je ne comprends pas? Voir l'animation ci-dessous.



Afficher le code
checkWin( cellX, cellY ){ var res = null; var newFig = getFig(cellX,cellY); if( ! newFig ) return false; var res; res = res || checkLine( cellX, cellY, 1, 0 ); //horizontal res = res || checkLine( cellX, cellY, 0, 1 ); //vertical res = res || checkLine( cellX, cellY, 1, 1 ); //diagonal 45 res = res || checkLine( cellX, cellY, 1, -1 ); //diagonal 135 return res; function getFig( x, y ){ return Model.Field[x] && Model.Field[x][y] ? Model.Field[x][y] : 'b'; } function checkLine( x, y, dx, dy ){ x = +x; y = +y; var score = 0; while( getFig( x - dx, y - dy ) == newFig ){ x -= dx; y -= dy; } while( getFig( x, y ) == newFig ){ x += dx; y += dy; score++; } if( score >= 5 ) return true; return false; } } 


Descendons au bot lui-mĂȘme


Donc, nous avons déjà écrit une page avec tic-tac-toe. Nous passons à la tùche principale - l'IA.
Vous ne pouvez pas simplement prendre et écrire du code si vous ne savez pas comment: vous devez réfléchir à la logique du bot.

L'essentiel est d'analyser le terrain de jeu, au moins une partie de celui-ci, et de calculer le prix (poids) de chaque cellule sur le terrain. La cellule avec le poids le plus élevé - la plus prometteuse - le bot y mettra un chiffre. La principale difficulté est de calculer le poids d'une cellule.

Terminologie


Les croix et les orteils sont des figures.
Une attaque sera appelĂ©e plusieurs personnages identiques cĂŽte Ă  cĂŽte sur la mĂȘme ligne. En fait, c'est beaucoup. Le nombre de piĂšces dans une attaque est sa puissance . Une piĂšce distincte est Ă©galement une attaque (puissance 1).

Sur les cellules d'attaque adjacentes (aux extrĂ©mitĂ©s), il peut y avoir des cellules vides ou des piĂšces ennemies. Il est logique de penser qu'une attaque avec deux cellules vides aux «extrĂ©mitĂ©s» peut se dĂ©velopper dans deux directions, ce qui la rend plus prometteuse. Le nombre de cellules vides aux "extrĂ©mitĂ©s" de l'attaque sera appelĂ© son potentiel . Le potentiel peut ĂȘtre 0, 1 ou 2.
Nous désignons les attaques comme suit: [puissance d'attaque, potentiel] . Par exemple, une attaque [4: 1] .


Figure 1. Attaque [4: 1]

Au cours de l'analyse, nous évaluerons toutes les cellules qui entrent dans une zone spécifique. Chaque cellule calculera son poids . Il est calculé sur la base du poids de toutes les attaques que cette cellule affecte.

L'essence de l'analyse


Imaginez que sur le terrain de jeu, il y a déjà plusieurs attaques d'un et du deuxiÚme joueur. Un des joueurs fait un mouvement (laissez les croix). Naturellement, il se déplace vers une cellule vide - et ainsi il peut:

  1. DĂ©veloppez votre attaque, et peut-ĂȘtre plus d'une, en augmentant sa puissance. Peut lancer une nouvelle attaque, etc.
  2. EmpĂȘchez le dĂ©veloppement d'une attaque ennemie ou bloquez-la complĂštement.

Autrement dit, notre protagoniste peut attaquer et dĂ©fendre. Ou peut-ĂȘtre d'un coup. Pour lui, le premier et le second sont importants.

L'essence de l'analyse est la suivante:

  1. Le bot substitue les chiffres de la cellule cochée: d'abord une croix, puis un zéro.
  2. Il recherche ensuite toutes les attaques reçues par de tels mouvements et résume leurs poids.
  3. Le montant reçu est le poids de la cellule.
  4. Un algorithme similaire est exécuté pour toutes les cellules du terrain de jeu.



En fait, nous vérifions avec un tel algorithme ce qui se passera si nous allons de cette façon ... et ce qui se passera si l'adversaire va de cette façon. Nous attendons avec impatience une étape et sélectionnons la cellule la plus appropriée - avec le poids le plus élevé.

Si une cellule a plus de poids qu'une autre, elle conduit à la création d'attaques plus dangereuses ou à bloquer de fortes attaques ennemies. Tout est logique ... il me semble.
Si vous allez sur la page et écrivez dans la console SHOW_WEIGHTS = true, vous pouvez sentir visuellement le fonctionnement de l'algorithme (les poids des cellules seront affichés).

Poids d'attaque


Je suis allé sur mon cerveau et apporté une telle correspondance d'attaques et de poids:

 ATTACK_WEIGHT = [[],[],[],[],[],[]]; ATTACK_WEIGHT[1][1] = 0.1; ATTACK_WEIGHT[2][1] = 2; ATTACK_WEIGHT[3][1] = 4; ATTACK_WEIGHT[4][1] = 6; ATTACK_WEIGHT[5][1] = 200; ATTACK_WEIGHT[1][2] = 0.25; ATTACK_WEIGHT[2][2] = 5; ATTACK_WEIGHT[3][2] = 7; ATTACK_WEIGHT[4][2] = 100; ATTACK_WEIGHT[5][2] = 200; ATTACK_WEIGHT[5][0] = 200; 

Empiriquement sĂ©lectionnĂ© - ce n'est peut-ĂȘtre pas la meilleure option.

J'ai ajouté une puissance d'attaque de 5 avec un poids prohibitif à la matrice. Cela peut s'expliquer par le fait que le bot analyse le jeu en regardant un pas en avant (en remplaçant la figure dans la cellule). Sauter une telle attaque n'est rien d'autre qu'une défaite. Eh bien, ou la victoire ... selon qui.

Les attaques à fort potentiel sont valorisées plus haut.

L'attaque [4: 2] dans la plupart des cas dĂ©cide du rĂ©sultat de la partie. Si le joueur a rĂ©ussi Ă  crĂ©er une telle attaque, l'adversaire ne pourra plus la bloquer. Mais ce n'est pas une victoire. L'ennemi peut terminer le jeu plus rapidement, mĂȘme si nous avons une attaque [4: 2] sur le terrain, donc son poids est infĂ©rieur Ă  celui des attaques d'une puissance de 5. Voir un exemple ci-dessous.


Figure 2. Attaque [4: 2]

Attaques déchirées


Le code n'est pas présenté dans ce paragraphe. Ici, nous introduisons le concept d'un diviseur d'attaque et expliquons l'essence des «attaques déchirées» .

Considérez la situation suivante: lorsque vous remplacez un chiffre pour supprimer plusieurs cellules vides, mais pas plus de 5, une autre est localisée.

Et, semble-t-il, deux figures identiques, sur la mĂȘme ligne ... visuellement cela ressemble Ă  une attaque, mais en fait non. Pas un ordre, car de telles attaques "dĂ©chirĂ©es" comportent Ă©galement une menace potentielle.

Surtout pour de tels cas, pour chaque attaque, nous calculerons le diviseur. Initialement, sa valeur est 1.

  1. Nous présentons l'attaque "déchirée" comme plusieurs
  2. On compte le nombre de cellules vides entre l'attaque centrale et le cÎté
  3. Pour chaque cellule vide, le diviseur est augmenté de 1
  4. Nous calculons le poids de l'attaque centrale comme d'habitude, le poids des attaques latérales - divisé par le diviseur


Fig 3. Analyse de "Attaque déchirée". Une cellule avec une croix jaune est scannée.

Ainsi, les attaques déchirées seront également prises en compte par l'IA. En fait, ce seront des attaques ordinaires, mais plus elles sont éloignées de la cellule scannée, moins elles ont d'influence sur elle et, par conséquent, elles ont moins de poids (grùce au diviseur).

Algorithme de recherche d'attaque


Créez d'abord une classe d' attaque. L'attaque aura 3 attributs, dont j'ai parlé plus tÎt:

 class Attack{ constructor( cap = 0, pot = 0, div = 1 ){ this.capability = cap; // this.potential = pot; // this.divider = div; // } 

Et une méthode qui renverra le poids d'une attaque donnée:

 countWeigth(){ return ATTACK_WEIGHT[ this.capability, this.potential ] / this.divider } } 

Ensuite. Nous diviserons la recherche de toutes les attaques pour une cellule en:

  1. Recherche horizontale
  2. Recherche verticale
  3. Recherche diagonale à 45 degrés
  4. Recherche en diagonale à 135 degrés

Ce sont toutes des lignes , et l'algorithme de recherche d'attaques sur ces lignes peut ĂȘtre gĂ©nĂ©ralisĂ©: la classe checkLine .

Cependant, nous n'avons pas besoin de vĂ©rifier toute la ligne. La puissance d'attaque maximale qui nous intĂ©resse est de 5. Bien sĂ»r, il est possible de crĂ©er une attaque avec une puissance de, disons, 6. Mais pour une IA qui analyse la situation de jeu du prochain coup, c'est la mĂȘme chose que 6 ou 5. La perspective d'obtenir l'une de ces attaques indique la fin du jeu au coup suivant. En consĂ©quence, le poids de la cellule analysĂ©e sera le mĂȘme dans les deux cas.

Attributs de classe:

 class checkLine{ constructor(){ //,        this.subFig = "×"; //     .    «0» - . this.Attacks = []; //  this.curAttack = new Attack; // (      ) this.iter = 1; //,     this.checkEdge = false; 

Il faut s'arrĂȘter lĂ , car la question peut se poser: pourquoi vĂ©rifier la 6e cellule si la puissance d'attaque maximale est 5. La rĂ©ponse est de dĂ©terminer le potentiel Ă©loignĂ© du centre d'attaque.

Voici un exemple: une attaque avec une puissance de 1 dans l'image se situe à la frontiÚre de la zone scannée. Pour découvrir le potentiel de cette attaque, vous devez «regarder à l'étranger».


Fig. 3. Numérisation des 6e cellules. Si vous ne scannez pas la 6e cellule, vous pouvez déterminer incorrectement le potentiel d'attaque.

  //   this.attackplace = 1; } 

Il n'y a peut-ĂȘtre tout simplement pas assez d'espace pour terminer certaines attaques. AprĂšs avoir comptĂ© le lieu de l'attaque, nous pouvons comprendre Ă  l'avance laquelle des attaques n'est pas prometteuse.


Fig. 4. Lieu d'attaque

L'algorithme est le suivant:

1) Commençons par la cellule centrale. Elle doit ĂȘtre vide (nous allons y faire un pas, non? Mais nous n'oublions pas que notre IA doit substituer les chiffres de cette cellule pour l'analyse du prochain coup. Le chiffre que nous substituons est this.subfig - la valeur par dĂ©faut est une croix). Étant donnĂ© que la cellule centrale contiendra initialement une certaine forme aprĂšs substitution, elle appartiendra Ă  une attaque this.curAttack :

  • sa puissance ne sera pas infĂ©rieure Ă  1 (un chiffre dans la cellule centrale)
  • diviseur - 1, car c'est une attaque centrale (elle appartient Ă  la cellule scannĂ©e);
  • le potentiel n'est pas encore connu - la valeur par dĂ©faut est 0;


Nous avons affiché tous ces points dans les valeurs par défaut du constructeur - voir le code ci-dessus.

2) Ensuite, en rĂ©duisant l'itĂ©rateur, itĂšre plus de 5 cellules d'un cĂŽtĂ© de celle numĂ©risĂ©e. La fonction getAttacks (cellX, cellY, subFig, dx, dy) en est responsable, oĂč:

cellX, cellY - coordonnées de la cellule cochée
subFig - le chiffre que nous substituons dans la cellule cochée
dx, dy - changements dans les coordonnées x et y dans les cycles - c'est ainsi que nous définissons la direction de recherche:

  • Horizontale (dx = 1, dy = 0)
  • Verticale (dx = 0, dy = 1)
  • Diagonale 45 (dx = 1, dy = -1)
  • Diagonale 135 (dx = 1, dy = 1)

Dans un sens, il s'agit d'un vecteur parallĂšle Ă  la ligne de recherche. Ainsi, une fonction pourra rechercher dans 4 directions et nous ne violerons pas le principe DRY Ă  nouveau.

Code de fonction:

 getAttacks( cellX, cellY, subFig, dx, dy ){ this.substitudeFigure( subFig ); //  –  ... for( var x = cellX - dx, y = cellY - dy; Math.abs( x - cellX ) <= 5 && Math.abs( y - cellY ) <= 5; x -= dx, y -= dy ) if( this.checkCell( x, y ) ) break; //: //    (  ) this.turnAround(); //  -    ... for( var x = cellX + dx, y = cellY + dy; Math.abs( x - cellX ) <= 5 && Math.abs( y - cellY ) <= 5; x += dx, y += dy ) if( this.checkCell( x, y ) ) break; return this.Attacks; } 

Veuillez noter que si checkCell () renvoie quelque chose, la boucle s'arrĂȘte.

3) Nous vérifions les chiffres de ces cellules.
La fonction checkCell (x, y) en est responsable:

Tout d'abord, Ă©crivez la forme dans la variable fig :
Model.Field est notre terrain de jeu.

 checkCell( x, y ){ var fig = Model.Field[x] && Model.Field[x][y] !== undefined ? Model.Field[x][y] : 'b'; 

fig peut ĂȘtre 'x', 'o', 'b' (bordure), 0 (cellule vide).

  • Si une telle figure coĂŻncide avec la figure de la cellule centrale ( this.subFig ), alors nous continuons l'algorithme - alors nous continuons Ă  scanner l'attaque, tout va bien, nous continuons dans le mĂȘme esprit. Une piĂšce supplĂ©mentaire dans l'attaque est un plus pour sa puissance ( this.curAttack.capability ) et sa place ( this.attackplace ).

    (Voir le code dans le paragraphe suivant)
  • S'il s'agit d'un chiffre diffĂ©rent, l'attaque que nous avons analysĂ©e auparavant (this.curAttack) est bloquĂ©e de ce cĂŽtĂ©. Nous ne changeons rien dans les paramĂštres d'attaque, l'Ă©crivons dans le tableau d'attaques et sortons de la boucle.

     if( fig == '○' || fig == '×' ){ if( this.subFig != fig ){ //  this.Attacks.push( this.curAttack ); //  return fig; //      } else{ //    this.curAttack.capability++; // +   this.attackplace++; // +   } } 

  • S'il n'y a pas de telles cellules, cela signifie qu'elles sont tombĂ©es hors de la limite du champ, ce qui signifie que l'attaque est bloquĂ©e. Nous l'Ă©crivons dans un tableau de toutes les attaques et sortons de la boucle.

     else if( fig == 'b' ){ // this.Attacks.push( this.curAttack ); return 'b'; } 

  • Si vous attrapez une cage vide, cela signifie que l'attaque en cours est terminĂ©e ou que nous avons affaire Ă  une "attaque dĂ©chirĂ©e". Plus le potentiel et le lieu d'attaque (car l'attaque n'est pas bloquĂ©e). Cependant, nous ne sortons pas de la boucle - c'est peut-ĂȘtre une "attaque dĂ©chirĂ©e" - nous Ă©crivons simplement this.curAttack dans le tableau de toutes les attaques de la ligne this.Attacks []. CrĂ©ez une nouvelle attaque «actuelle» et augmentez son diviseur de 1 (il s'agit d'une attaque latĂ©rale).

     else{ //  if( this.curAttack.capability ){ this.curAttack.potential++; this.Attacks.push( this.curAttack ); this.curAttack = new Attack; this.curAttack.potential++; } this.curAttack.divider++; this.attackplace++; } 



4) Si sur la 5Úme cellule le chiffre coïncide avec la cellule centrale, alors l'attaque "reposait" contre la frontiÚre et pour déterminer le potentiel d'attaque, vous devrez "vérifier la frontiÚre" ( this.checkEdge = true ).

 if( this.iter == 4 && fig == this.subFig ) // 5-  this.checkEdge = true; else if( this.iter == 5 ){ if( this.checkEdge ){ if( fig == this.curFig || fig == 0 ) this.curAttack.potential++; this.Attacks.push( this.curAttack ) } return 0; } this.iter++ 

La fonction checkCell est prĂȘte. Cependant, nous continuons Ă  travailler sur la classe checkLine .

5) AprÚs avoir terminé le premier cycle, vous devez "faire demi-tour". Nous traduisons l'itérateur au centre et l'attaque centrale, avec l'index 0, le supprimons du tableau d'attaques et le définissons comme l'actuel.

 turnAround(){ this.iter = 1; this.checkEdge = false; this.curAttack = this.Attacks[0]; this.Attacks.splice(0,1) } 

6) Ensuite, allez de l'autre cÎté de la cellule actuelle, augmentant l'itérateur.
Absolument le mĂȘme contrĂŽle des chiffres. (Code dĂ©jĂ  Ă©crit - fonction getAttacks )

7) Tout, nous avons rassemblĂ© toutes les attaques qui Ă©taient sur la ligne dans un mĂȘme tableau.
C'est tout avec la classe checkLine ... tout est fait.

Eh bien, alors tout est simple - créez un objet checkLine pour chacune des lignes (2 diagonales, horizontale et verticale) et appelez la fonction getAttacks . Autrement dit, pour chaque ligne - son propre objet checkLine et, par conséquent, son propre ensemble d'attaques.

Que la fonction getAllAttacks () soit responsable de tout cela - déjà séparément des classes décrites ci-dessus;

 getAllAttacks( cellX, cellY ){ // ,  , //       if( Model.Field[ cellX ][ cellY ] ) return false var cX = []; var cO = []; //   ... cX['0'] = this.getAttacksLine( cellX, cellY, '×', 1, 0 ); cX['90'] = this.getAttacksLine( cellX, cellY, '×', 0, 1 ); cX['45'] = this.getAttacksLine( cellX, cellY, '×', 1, -1 ); cX['135'] = this.getAttacksLine( cellX, cellY, '×', 1, 1 ); //  ... cO['0'] = this.getAttacksLine( cellX, cellY, '○', 1, 0 ); cO['90'] = this.getAttacksLine( cellX, cellY, '○', 0, 1 ); cO['45'] = this.getAttacksLine( cellX, cellY, '○', 1, -1 ); cO['135'] = this.getAttacksLine( cellX, cellY, '○', 1, 1 ); return { //     'x': cX, 'o': cO } } getAttacksLine( cellX, cellY, subFig, dx, dy ){ //      var C = new checkLine; C.getAttacks( cellX, cellY, subFig, dx, dy ); return this.filterAttacks( C ) //   } 

En sortie, nous avons un objet avec toutes les attaques pour la cellule testée

Cependant, vous avez peut-ĂȘtre remarquĂ© une sorte de fonction de filtre. Sa tĂąche est de filtrer les attaques «futiles»:

  • Avec une puissance nulle (on ne sait jamais s'ils entrent dans le rĂ©seau)
  • Attaques qui manquent d'espace (lieu d'attaque <5)
  • Avec un potentiel nul.

Cependant, si l'attaque a une puissance supérieure à 5, le filtre la sautera. Le bot doit voir de telles attaques, leur dépistage entraßnera des jambages en fin de partie.

 filterAttacks( attackLine ){ var res = [] if( attackLine.attackplace >= 5 ) attackLine.Attacks.forEach( ( a )=>{ if( a.capability && a.potential || a.capability >= 5 ) res.push( a ) }) attackLine.Attacks = res; return res } 

Points d'arrĂȘt


Oui ... encore une fois, désolé! Nous appellerons donc la situation dans le jeu, lorsqu'un mauvais coup décide du résultat du jeu.

Par exemple, une attaque [3: 2] est un point d'arrĂȘt. Si l'adversaire ne le bloque pas en plaçant une piĂšce Ă  cĂŽtĂ© de lui, alors au prochain coup, nous avons dĂ©jĂ  une attaque [4: 2] sur le terrain de jeu - eh bien, le rĂ©sultat du jeu est dĂ©cidĂ©, quoi qu'on puisse dire (dans la grande majoritĂ© des cas).

Ou une attaque [4: 1]. Un bĂąillement - et le jeu peut ĂȘtre facilement terminĂ©.


Figure 5. Point d'arrĂȘt

Tout est clair et comprĂ©hensible, et l'algorithme dĂ©crit ci-dessus est dĂ©jĂ  capable de prendre en compte les points d'arrĂȘt et de les bloquer en temps opportun. Le bot attend avec impatience. Il verra qu'au prochain tour, l'adversaire est capable de crĂ©er une attaque [5: 1], par exemple, dont le poids est de 200 - ce qui signifie que le ringard rusĂ© ira ici.

Cependant, imaginez une situation oĂč l'un des joueurs parvient Ă  obtenir 2 points d'arrĂȘt sur le terrain. Et cela, Ă©videmment, ne laisse aucune chance Ă  l'adversaire, car d'un seul coup, nous ne pouvons bloquer qu'un seul point d'arrĂȘt. Comment apprendre Ă  notre IA Ă  bloquer de telles attaques?


Figure 6. 2 points d'arrĂȘt

Tout est simple, lors de l'analyse d'une cellule, en y substituant une piĂšce, nous compterons le nombre de points d'arrĂȘt que nous obtiendrons au prochain coup (le bot regarde le pas en avant, n'oubliez pas). En comptant 2 points d'arrĂȘt, nous augmentons le poids des cellules de 100.

Et maintenant, le bot empĂȘchera non seulement de telles situations de jeu, mais pourra Ă©galement les crĂ©er, ce qui en fait maintenant un adversaire plus redoutable.

Comment comprendre qu'une attaque est un point d'arrĂȘt


Commençons par l'Ă©vidence: toute attaque d'une puissance de 4 est un point d'arrĂȘt. Un seul coup manquĂ© nous donne la possibilitĂ© de terminer le jeu, c'est-Ă -dire mettre 5 morceaux d'affilĂ©e.

De plus, si le potentiel d'attaque est de 2, alors nous dĂ©penserons 1 tour de plus pour bloquer une telle attaque, ce qui signifie qu'il y a un point d'arrĂȘt d'une puissance de 3. Mais il n'y a qu'un seul point d'arrĂȘt - c'est une attaque [3: 2].

Et encore plus difficile - "attaques déchirées" .
Nous ne considĂ©rerons que les attaques avec une cellule vide au milieu - pas plus. En effet, pour terminer l'attaque avec deux cellules vides au milieu, vous devez dĂ©penser au moins 2 mouvements - ce n'est clairement pas un point d'arrĂȘt.

Comme nous nous en souvenons, nous considérons les attaques déchirées comme plusieurs attaques classiques: une attaque centrale et des attaques latérales. L'attaque centrale appartient à la cellule scannée, le diviseur latéral en a plus de 1 - cela a été décrit ci-dessus.

Algorithme pour trouver un point d'arrĂȘt (plus facile, lire ci-dessous):

  1. Nous introduisons le score variable
  2. On prend l'attaque centrale, on considĂšre le pouvoir
  3. Nous prenons l'un des cÎtés si son diviseur n'est pas plus de 2x.
  4. Score - la somme de la puissance des attaques centrales et latérales
  5. Si les potentiels des attaques centrales et latérales sont de 2, alors pour bloquer une telle attaque, vous devez passer un tour de plus. Par conséquent, le score est augmenté de 1
  6. Si score > = 4, alors c'est un point d'arrĂȘt
    En fait, les points d'arrĂȘt pouvaient simplement ĂȘtre Ă©numĂ©rĂ©s, il n'y en a pas beaucoup, mais je n'ai pas tout de suite compris cela.

 isBreakPoint( attackLine ){ if( ! attackLine || ! attackLine.length ) return false; var centAtk; attackLine.forEach( ( a )=>{ if( a.divider == 1 ) centAtk = a; }) if( centAtk.capability >= 4 ) return true if( centAtk.potential == 2 && centAtk.capability >= 3 ) return true; var res = false; attackLine.forEach( ( a )=>{ var score = centAtk.capability; if( a.divider == 2 ){ //side attack if( centAtk.potential == 2 && a.potential == 2 ) score++; if( score + a.capability >= 4 ){ res = true; return; } } }) return res; } 

Oui, nous allons enfin tout rassembler


Ainsi, l'enfer principal derriÚre est décrit ci-dessus. Il est temps de façonner quelque chose qui fonctionne à partir de cela. Fonction countWeight (x, y) - prend les coordonnées de la cellule en entrée et renvoie son poids. Qu'y a-t-il sous sa capuche?

Tout d'abord, nous obtenons un tableau de toutes les attaques auxquelles appartient la cellule. ( getAllAttacks (x, y) ). En parcourant toutes les lignes, nous comptons le nombre de points d'arrĂȘt. S'il y a 2 points d'arrĂȘt, nous rappelons qu'une telle situation peut dĂ©cider du rĂ©sultat du jeu et augmenter le poids des cellules de 100.
Cependant, tous les points d'arrĂȘt doivent appartenir Ă  un seul joueur, j'ai donc dĂ» implĂ©menter une vĂ©rification en 2 Ă©tapes: d'abord les croix, puis les zĂ©ros.

Étant donnĂ© que dans la gamme de poids d'attaque ( ATTACK_WEIGHTS [] ), je n'ai pas fourni d'attaques d'une puissance de 6 ou plus, j'ai dĂ» les remplacer par des attaques d'une puissance de 5. Cela ne fait aucune diffĂ©rence - ils mĂšnent tous Ă  la fin du jeu.

Eh bien, nous résumons les poids d'attaque - c'est tout.

Autre petit point: pour que le bot ne soit pas stupide en fin de partie, quand il a déjà construit une attaque avec une puissance de 4 et pense au coup en cours, il faut augmenter significativement le poids de la cellule pour terminer une telle attaque. Sans cela, l'IA peut tout simplement commencer à se défendre contre les attaques «dangereuses» de l'adversaire, bien que le jeu semble gagné. Le dernier mouvement est important.

 countWeight( x, y ){ var attacks = this.getAttacks( x, y ) if( ! attacks ) return; var sum = 0; sum += count.call( this, attacks.x, '×' ); sum += count.call( this, attacks.o, '○' ); return sum function count( atks, curFig ){ var weight = 0; var breakPoints = 0; [ "0", "45", "90", "135" ].forEach( ( p )=>{ if( this.isBreakPoint( atks[p] ) ){ debug( "Break point" ) if( ++breakPoints == 2 ){ weight += 100; debug( "Good cell" ) return; } } atks[p].forEach( ( a )=>{ if( a.capability > 5 ) a.capability = 5; if( a.capability == 5 && curFig == Model.whoPlays.char ) weight += 100; weight += a.getWeight(); }); }) return weight } } 

Maintenant, lors de l'appel de cette fonction pour une cellule spécifique, nous obtiendrons son poids. Nous effectuons cette opération pour toutes les cellules et sélectionnons la meilleure (avec le poids le plus élevé). Là et allez)

Vous pouvez trouver le reste du code sur github . Il y a déjà beaucoup de matériel, et sa présentation, comme je n'ai pas essayé, laisse beaucoup à désirer. Mais si vous pouviez lire jusqu'ici, cher lecteur, je vous en serais reconnaissant.


Descends! Oui, vous pouvez le battre, mais le faire est un peu problĂ©matique pour moi personnellement. Peut-ĂȘtre que je ne fais pas assez attention. Essayez aussi votre force.

Je sais que c'est plus facile, mais je ne sais pas comment. Je voudrais écouter les gens qui connaissent ou regardent d'autres implémentations d'un tel bot.

Je sais ce qui peut ĂȘtre mieux. Oui ... vous pouvez utiliser des algorithmes bien connus, tels que minimax, mais pour cela, vous devez avoir une base de connaissances dans le domaine de la thĂ©orie des jeux, dont je ne peux malheureusement pas me vanter.

À l'avenir, je prĂ©vois d'ajouter une analyse des points d'arrĂȘt Ă  plusieurs Ă©tapes, ce qui fera du bot un adversaire encore plus sĂ©rieux. Cependant, maintenant je n'ai pas une idĂ©e claire de la mise en Ɠuvre de cela; J'ai juste la prochaine session et un diplĂŽme incomplet - ce qui m'attriste.

Merci si vous lisez jusqu'au bout.

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


All Articles