¿Cómo juega una computadora el ajedrez?
Hikaru Nakamura, quien recientemente desafió una computadora,una computadora ha vencido por mucho tiempo a un hombre en el ajedrez, ahora los jugadores de ajedrez más fuertes no son capaces de vencer incluso a una computadora portátil vieja. Ahora los motores de ajedrez se utilizan para analizar juegos, buscar nuevas opciones y jugar por correspondencia.Si está interesado en cómo están organizados los motores de ajedrez, bienvenido a cat.Introduccion
Una vez que estuve seguro de que los programas de ajedrez (también son motores, pero más sobre eso más adelante) solo tenga en cuenta la gran cantidad de juegos jugados y encuentre su posición actual en ellos y haga el movimiento correcto. En mi opinión, lo leí en algún libro.Esta es, sin duda, una opinión muy ingenua. Se puede obtener una nueva posición en el ajedrez con el décimo movimiento. Aunque hay menos posiciones en el ajedrez que en el juego , sin embargo, después de 3 movimientos (un movimiento es un movimiento de blanco y negro, un medio movimiento es un movimiento de un solo lado), el árbol de movimientos consta de casi 120 millones de nodos. Además, el tamaño del árbol después de 14 medios movimientos desde la posición inicial ha sido considerado por los entusiastas durante más de un año, hasta ahora ha avanzado en aproximadamente un tercio.También pensé que los programas de ajedrez, a pesar de la larga dataganar el partido sobre el campeón mundial todavía está al alcance de las mejores personas. Esto tampoco es cierto.En un reciente mini-partido humano-máquina, Hikaru Nakamura , uno de los jugadores de ajedrez más fuertes del mundo, jugó con Komodo , uno de los (dos) programas de ajedrez más fuertes del mundo. El programa se lanzó en un Xeon de 24 núcleos. Como las personas ya no pueden competir en igualdad de condiciones con una computadora, el gran maestro se adelantó en cada uno de los 4 juegos:- En el primer juego: un peón y un movimiento: la computadora jugaba de negro y sin peón f7
- En el segundo, solo un peón: la computadora jugó en blanco sin un peón f2
- En la tercera calidad (la diferencia entre una torre y una pieza liviana se estima en aproximadamente 2 peones): una computadora blanca sin torre a1, un hombre sin caballero b8 y una torre a8 en su lugar.
- En el cuarto - cuatro movimientos: una persona juega en blanco y en lugar del primer movimiento realiza 4 movimientos sin cruzar el centro del tablero.
Hubo ciertas disputas con respecto a la discapacidad, por ejemplo, la ausencia del peón f debilita un poco al rey, pero después del enroque le da una línea abierta a la torre. La ausencia de un peón central probablemente da una mayor ventaja. 4 movimientos dan una buena ventaja posicional, pero si juegas un debut cerrado como la vieja defensa india, entonces esta ventaja no es tan difícil de anular.Además, los juegos se jugaron con un control de 45 "+15 ', es decir, 45 minutos por juego y 15 segundos de adición.cada movimiento Por lo general, los controles más cortos brindan una ventaja adicional a la computadora, mientras que los más largos aumentan ligeramente las posibilidades de una persona. Incluso en una fracción de segundo, la computadora logrará barrer movimientos de pérdida abierta, mientras que debido al crecimiento exponencial del árbol de variantes, cada mejora posterior en el análisis lleva más tiempo.Sin embargo, hubo una desventaja y la persona perdió en el partido 2.5-1.5, después de haber empatado los primeros 3 juegos y perdido el cuarto. Al mismo tiempo, el gran maestro débil ganó con bastante confianza.con una desventaja de 2 peones. Por lo tanto, la ventaja de los mejores programas sobre las mejores personas en este momento es entre 1 y 2 peones de la desventaja. Por supuesto, esta evaluación es muy aproximada, pero para una evaluación precisa es necesario jugar varios miles de juegos entre personas y programas, y es poco probable que alguien lo haga. Tenga en cuenta que la calificación ELO, a menudo indicada para programas, no tiene nada que ver con la calificación de las personas.¿Qué es un motor de ajedrez?
Para que una persona pueda jugar al ajedrez con una computadora, además de buscar el mejor movimiento, necesita una GUI. Afortunadamente, se inventó una interfaz universal (incluso dos, Winboard y UCI , pero la mayoría de los motores usan UCI) para la comunicación entre la GUI y el propio programa de ajedrez (motor). Por lo tanto, los programadores pueden centrarse en el algoritmo del juego de ajedrez, sin pensar en la interfaz. La otra cara de la moneda es que crear una GUI es mucho más aburrido que escribir un motor, luego las GUI gratuitas pierden notablemente las pagas. A diferencia de los motores, donde Stockfish gratis está luchando con confianza por la primera línea de la calificación con Komodo pagado.¿Cómo siguen jugando?
Entonces, ¿cómo funciona un motor de ajedrez moderno?Presentación de la Junta
La base de cualquier motor es la representación de un tablero de ajedrez. En primer lugar, es necesario "explicar" a la computadora todas las reglas del ajedrez y darle la oportunidad de mantener la posición de ajedrez. Sin esto, es imposible evaluar la posición y hacer movimientos.Hay dos formas principales de almacenar una representación de un tablero: por formas o por celdas . En el primer caso, almacenamos para cada pieza su lugar en el tablero, en el segundo, por el contrario, para cada celda almacenamos lo que está allí. Cada método tiene sus ventajas y desventajas, pero en este momento todos los motores principales usan la misma representación de la placa: las tablas de bit.Bitboards
Afortunadamente, hay 64 celdas en el tablero de ajedrez. Entonces, si usamos un bit para cada celda, podemos almacenar toda la placa en un entero de 64 bits.En una variable almacenaremos todas las piezas blancas, en otra, todas negras, y en 6 más, cada tipo de figuras por separado (otra opción - 12 bitboards para cada color y tipo de figuras por separado).¿Cuál es la ventaja de esta opción?El primero es la memoria. Como aprendemos más adelante, durante el análisis, la representación de la placa se copia muchas veces y, en consecuencia, la RAM se consume. Los bitboards son una de las representaciones de tablero de ajedrez más compactas.En segundo lugar, la velocidad. Muchos cálculos, por ejemplo, el cálculo de posibles movimientos, se reducen a varias operaciones de bits. Debido a esto, por ejemplo, el uso de la instrucción POPCNT proporciona ~ 15% de aceleración a los motores modernos. Además, durante la existencia de bitboards, se inventaron muchos algoritmos y optimizaciones, como, por ejemplo, bitboards "mágicos" .Buscar
Minimax
En el corazón de la mayoría de los motores de ajedrez se encuentra el algoritmo de búsqueda minimax o su modificación de no hamax. En resumen, bajamos por el árbol, evaluamos las hojas y luego subimos, cada vez que elegimos el movimiento óptimo para el jugador actual, minimizamos el puntaje para uno (negro) y maximizamos para el segundo (blanco). De ahí el nombre. Una vez en la raíz, obtenemos una secuencia de movimientos que es óptima para ambos jugadores. La diferencia entre minimax y no hamax es que en el primer caso, nos turnamos para elegir los movimientos con las clasificaciones máximas y mínimas, y en el segundo, en cambio, cambiamos el signo de todas las clasificaciones y siempre elegimos el máximo (entendimos de dónde vinieron). Más detalles aquí y aquí .Alfa beta
La primera optimización es alfa beta . La idea de alfa-beta es simple: si ya tengo un buen movimiento, entonces puedes cortar los movimientos, que obviamente son peores. Considere el ejemplo en la imagen espeluznante a la izquierda. Supongamos que el jugador A tiene 2 movimientos posibles: a3 y b3. Después de analizar el curso de a3, el programa recibió una calificación de +1.75. Comenzando a evaluar el movimiento b3, el programa vio que el jugador B tiene dos movimientos: a6 y a5. Evaluación del curso a6 +0.5. Como el jugador B elige un movimiento con un puntaje mínimo, no elegirá un movimiento con un puntaje mayor a 0.5, lo que significa que la estimación del movimiento b3 es menor a 0.5, y no tiene sentido considerarlo. Por lo tanto, el subárbol restante de b3 se corta.Para el recorte, almacenamos los límites superior e inferior: alfa y beta. Si durante el análisis un movimiento obtiene una puntuación más alta que beta, entonces el nodo actual se corta. Si la puntuación es más alta que alfa, entonces alpha se actualiza.Los nodos en alfa beta se dividen en 3 categorías:- PV-Nodes : nodos cuya evaluación cayó en la ventana (entre alfa y beta). La raíz y el nodo más a la izquierda son siempre nodos de este tipo.
- Nodos de corte (o nodos de falla alta ): nodos en los que se produjo el corte beta.
- Todos los nodos (o nodos de falla baja ): nodos en los que ningún movimiento superó el alfa según la evaluación.
Movimientos de clasificación
Cuando se utiliza alfa beta, el orden de los movimientos se vuelve importante. Si podemos poner el mejor movimiento primero, los movimientos restantes se analizarán mucho más rápido debido a los cortes beta.Además de usar el hash y el mejor movimiento de la iteración anterior, existen varias técnicas para ordenar los movimientos.Para capturas, por ejemplo, se puede usar un simple MVV-LVA heurístico (Víctima más valiosa - Agresor menos valioso). Ordenamos todas las capturas en orden descendente del valor de la "víctima", y dentro ordenamos de nuevo en orden ascendente del valor del "agresor". Obviamente, generalmente es más rentable recoger a la reina por peón que viceversa.Para los movimientos "silenciosos" se utiliza el método de movimientos "asesinos", movimientos que causaron el corte beta. Estos movimientos generalmente se verifican inmediatamente después de los movimientos desde el hash y las capturas.Tablas hash o tablas de permutación
A pesar del gran tamaño del árbol, muchos nodos son idénticos. Para no analizar la misma posición dos veces, la computadora almacena los resultados del análisis en una tabla y cada vez verifica si ya hay un análisis listo de esta posición. Por lo general, dicha tabla almacena el hash real de la posición, la calificación, el mejor movimiento y la edad de calificación. Se requiere edad para reemplazar las viejas posiciones al llenar la tabla.Búsqueda iterativa
Como saben, si no podemos analizar todo el árbol por completo, minimax necesita una función de evaluación. Luego de haber alcanzado cierta profundidad, detenemos la búsqueda, evaluamos la posición y comenzamos a trepar al árbol. Pero dicho método requiere una profundidad predeterminada y no proporciona resultados intermedios de alta calidad.La búsqueda iterativa resuelve estos problemas. Primero, analizamos a una profundidad de 1, luego a una profundidad de 2, etc. Por lo tanto, cada vez que bajamos un poco más profundo que la última vez, hasta que se detiene el análisis. Para reducir el tamaño del árbol de búsqueda, los resultados de la última iteración generalmente se usan para cortar movimientos deliberadamente malos en el actual. Este método se llama ventana de aspiración y se usa universalmente.Búsqueda de reposo
Este método está diseñado para combatir el "efecto horizonte". Simplemente detener la búsqueda a la profundidad adecuada puede ser muy peligroso. Imagina que nos detuvimos en medio del intercambio de reinas: el blanco tomó a la reina negra, y en el siguiente movimiento, el negro debería elegir blanco. Pero en este momento en el tablero, las blancas tienen una reina extra y una evaluación estática será fundamentalmente incorrecta.Para hacer esto, antes de hacer una evaluación estática, verificamos todas las capturas (a veces también las fichas) y bajamos del árbol a una posición en la que no hay posibles capturas y fichas. Naturalmente, si todas las capturas empeoran la estimación, entonces devolvemos la estimación de la posición actual.Búsqueda selectiva
La idea de una búsqueda selectiva es tomar más tiempo para considerar movimientos "interesantes" y menos para considerar poco interesante. Para hacer esto, use extensiones que aumenten la profundidad de la búsqueda en ciertas posiciones y abreviaturas que reduzcan la profundidad de la búsqueda.La profundidad aumenta en el caso de capturas, fichas, si el movimiento es el único o mucho mejor que las alternativas o si hay un peón pasado.Recorte y corte
Con cortes y cortes, todo es mucho más interesante. Pueden reducir significativamente el tamaño del árbol.Brevemente sobre el recorte:- - — , . , , . , , , , .
- — , -. , , . (1-2).
- — , , . . PV . .
- Multi-Cut — M(, 6) C(, 3) Cut-node, .
- null- — null- ( ) , . , , , , .
Las abreviaturas se usan cuando no estamos tan seguros de que el movimiento sea malo y, por lo tanto, no lo corte, simplemente reduzca la profundidad. Por ejemplo, la maquinilla de afeitar es una abreviatura siempre que la estimación estática de la posición actual sea menor que alfa.Debido a la clasificación de movimientos y cortes de alta calidad, los motores modernos logran alcanzar un coeficiente de ramificación por debajo de 2 . Debido a esto, desafortunadamente, a veces no notan víctimas y combinaciones no estándar.NegaScout y PVS
Dos técnicas muy similares que utilizan el hecho de que después de encontrar el nodo PV (suponiendo que nuestros movimientos estén bastante bien ordenados), lo más probable es que no cambie, es decir, todos los nodos restantes devolverán una calificación más baja que alfa. Por lo tanto, en lugar de buscar con una ventana de alfa a beta, buscamos con una ventana de alfa a alfa + 1, lo que nos permite acelerar la búsqueda. Por supuesto, si en algún nodo obtenemos clipping beta, entonces debe ser re-apreciado, ya por una búsqueda normal.La diferencia entre los dos métodos está solo en la redacción: se desarrollaron aproximadamente al mismo tiempo, pero de forma independiente, y por lo tanto se conocen con diferentes nombres.Búsqueda paralela
La paralelización de alfa beta es un gran tema separado. Lo examinaré brevemente y, a quién le importa, consulte la Búsqueda paralela alfa-beta en multiprocesadores de memoria compartida . La dificultad es que en una búsqueda paralela, muchos nodos de corte se analizan antes de que otro hilo encuentre una refutación (instala una beta), mientras que en una búsqueda secuencial, con una buena clasificación, muchos de estos nodos se cortarían.Lazy SMP
Un algoritmo muy simple. Simplemente comenzamos todos los hilos al mismo tiempo con la misma búsqueda. La comunicación de flujos se produce debido a la tabla hash. Lazy SMP fue sorprendentemente eficaz, tanto que el Stockfish de gama alta cambió a YBW. Es cierto que algunos creen que la mejora se debió a la implementación deficiente de YBWC y al recorte demasiado agresivo, y no a la ventaja de Lazy SMP.Concepto de espera de hermanos jóvenes (YBWC)
El primer nodo (hermano mayor) debe analizarse completamente, después de lo cual se inicia un análisis paralelo de los nodos restantes (hermanos menores). La idea es la misma, el primer movimiento mejorará significativamente alfa o incluso le permitirá cortar todos los demás nodos.División dinámica de árboles (DTS)
Algoritmo rápido y complejo. Un poco sobre la velocidad: la velocidad de búsqueda se mide a través de ttd (tiempo hasta la profundidad), es decir, el tiempo durante el cual la búsqueda alcanza una cierta profundidad. Este indicador generalmente se puede usar para comparar el trabajo de diferentes versiones de un motor o un motor que funciona con un número diferente de núcleos (aunque Komodo, por ejemplo, aumenta el ancho del árbol con más núcleos disponibles). Además, durante la operación, el motor muestra la velocidad de búsqueda en nps (nodos por segundo). Esta métrica es mucho más popular, pero ni siquiera permite que el motor se compare a sí mismo. La SMP perezosa, en la que no hay sincronización, aumenta casi nps linealmente, pero debido a la gran cantidad de trabajo innecesario, su ttd no es tan impresionante. Mientras que para DTS, nps y ttd cambian casi lo mismo .Para ser honesto, todavía no podía entender completamente este algoritmo, que, a pesar de su alta eficiencia, se usa literalmente en un par de motores. Para quien es muy interesante, siga el enlace de arriba.Calificación
Entonces, hemos alcanzado la profundidad necesaria, buscamos la calma y, finalmente, necesitamos evaluar la posición estática.La computadora evalúa la posición en peones: +1.0 significa que las blancas tienen una ventaja igual a 1 peón, -0.5 significa que las negras tienen la ventaja de medio peón. El tapete se estima en 300 peones, y la posición en la que se conoce el número de movimientos al tapete x es en (300-0.01x) peones. +299.85 significa que las blancas se emparejan en 15 movimientos. En este caso, el programa en sí mismo generalmente funciona con estimaciones completas en centipes (1/100 peones).¿Qué parámetros tiene en cuenta la computadora al evaluar una posición?Material y movilidad.
Lo mas simple. La reina tiene 9-12 peones, la torre 5-6, el caballero y el alfil 2.5-4 y el peón, respectivamente, un peón. En general, el material es una heurística digna para evaluar una posición y cualquier ventaja posicional generalmente se transforma al final en una material.La movilidad se considera simple: el número de movimientos posibles en la posición actual. Cuantos más, más móvil es el ejército del jugador.Tablas de posición de forma
El caballero en la esquina del tablero suele ser malo, los peones más cercanos a la retaguardia enemiga son cada vez más valiosos, etc. Para cada figura, se compila una tabla de bonificaciones y penalizaciones dependiendo de su posición en el tablero.Estructura de peón
- Peones dobles : dos peones en la misma vertical. A menudo es difícil defenderlos con otros peones, se considera una debilidad.
- — , . , .
- — , . ,
- — , . , .
Todos los parámetros anteriores afectan la evaluación del juego de diferentes maneras, dependiendo de la etapa del juego. En la apertura no tiene sentido el peón pasado, pero en el juego final debes llevar al rey al centro del tablero y no esconderte detrás de los peones.Por lo tanto, muchos motores tienen una clasificación separada para el final del juego y para el debut. Evalúan la etapa del juego según el material restante en el tablero y, de acuerdo con esto, consideran la calificación: cuanto más cerca del final del juego, menos se ve afectado el puntaje inicial y más es el final del juego.Otros
Además de estos factores básicos, los motores pueden agregar algunos otros factores a la evaluación, por ejemplo, la seguridad del rey, las piezas bloqueadas, las islas de peones, el control del centro, etc.Calificación precisa o búsqueda rápida?
Disputa tradicional: que es más efectiva, evalúa con precisión la posición o logra una mayor profundidad de búsqueda. La experiencia ha demostrado que las funciones de evaluación excesivamente "pesadas" son ineficaces. Por otro lado, una evaluación más detallada, teniendo en cuenta más factores, generalmente conduce a un juego más "bello" y "agresivo".Libros de debut y mesas finales
Libros de debut
En los albores del ajedrez informático, los programas tuvieron su debut muy débilmente. Debut a menudo requiere decisiones estratégicas que afectarán todo el juego. Por otro lado, la teoría de la apertura estaba bien desarrollada en las personas, la apertura se analizaba repetidamente y se jugaba de memoria. Entonces se creó una "memoria" similar para las computadoras. A partir de la posición inicial, se construyó un árbol de movimientos y se evaluó cada movimiento. Durante el juego, el motor simplemente eligió uno de los movimientos "buenos" con cierta probabilidad.Desde entonces, los libros de debut han crecido, muchos debuts se analizan usando computadoras hasta el final del juego. No hay necesidad de ellos, los motores fuertes han aprendido a tocar en el debut, pero están abandonando las líneas principales con bastante rapidez.Mesas finales
De vuelta a la introducción. Recuerde la idea de almacenar muchas posiciones en la memoria y elegir la correcta. Ahí está ella. Para un número pequeño (hasta 7) de cifras, se calculan todas las posiciones existentes. Es decir, en estas posiciones la computadora comienza a jugar perfectamente, ganando en el número mínimo de movimientos. Menos: el tamaño y el tiempo de generación. La creación de estas tablas ayudó en el estudio de los finales.Generación de tablas
Generamos todas las posiciones posibles (teniendo en cuenta la simetría) con un cierto conjunto de formas. Entre ellos encontramos y designamos todas las posiciones donde se encuentra el tapete. En el próximo pase, denotamos todas las posiciones en las que puede colocarse en posiciones con un tapete; en estas posiciones, se coloca un tapete en 1 turno. Así encontramos todas las posiciones con un compañero 2,3,4, 549 movimientos. En todas las posiciones sin marcar: un empate.Tablas de Nalimov
Las primeras tablas finales publicadas en 1998. Para cada posición, se almacenan el resultado del juego y el número de movimientos al tapete con un juego ideal. El tamaño de todas las terminaciones de seis cifras es de 1.2 terabytes.Mesas de Lomonosov
En 2012, todas las terminaciones de siete cifras (excepto 6 versus 1) se contaron en la supercomputadora Lomonosov en la Universidad Estatal de Moscú . Estas bases están disponibles solo por dinero y estas son las únicas tablas completas de finales de siete cifras existentes.Syzygy
El estándar de facto. Estas bases son mucho más compactas que las bases de Nalimov. Se componen de dos partes: WDL (Win Draw Lose) y DTZ (Distance to zeroing). Las bases de datos WDL están destinadas para su uso durante la búsqueda. Una vez que el nodo del árbol se encuentra en la tabla, tenemos el resultado exacto del juego en esta posición. Las DTZ están diseñadas para usarse en la raíz: almacenan el número de movimientos en un contador de anulación de los movimientos (movimiento por peón o captura). Por lo tanto, las bases WDL son suficientes para el análisis, y las bases DTZ pueden ser útiles para analizar los finales. Syzygy es mucho más pequeña: 68 gigabytes para WDL de seis figuras y 83 para DTZ. No hay bases de siete cifras, ya que su generación requiere aproximadamente terabytes de RAM.Uso
Las tablas finales se usan principalmente para el análisis, el aumento en la fuerza de los motores del juego es pequeño: 20-30 puntos ELO . Sin embargo, dado que la profundidad de búsqueda de los motores modernos puede ser muy grande, las consultas para las bases finales del árbol de búsqueda aún ocurren en el debut.Otro interesante
Jirafas o redes neuronales juegan al ajedrez
Algunos de ustedes pueden haber oído hablar de un motor de ajedrez en redes neuronales que ha alcanzado el nivel de IM (que, como entendimos en la introducción, no es tan bueno para el motor). Fue escrito y publicado en Bitbucket por Matthew Lai, quien desafortunadamente dejó de trabajar en él debido al hecho de que comenzó a trabajar en Google DeepMind .Parámetros de ajuste
Agregar una nueva función al motor no es difícil, pero ¿cómo puedo verificar que dio amplificación? La opción más simple es jugar varios juegos entre la versión antigua y la nueva y ver quién gana. Pero si la mejora es pequeña, y generalmente ocurre después de que se hayan agregado todas las características principales, debería haber varios miles de juegos, de lo contrario no habrá confiabilidad.Stockfish
Hay muchas personas trabajando en este motor, y cada una de sus ideas necesita ser verificada. Con la fuerza actual del motor, cada mejora da un aumento de un par de puntos de calificación, pero al final, se obtiene un aumento constante de varias decenas de puntos al año.Su solución es típica para el código abierto: los voluntarios brindan su poder para conducir cientos de miles de juegos en ellos.CLOP
Un programa que optimiza los parámetros a través de la regresión lineal utilizando los resultados de juegos de motor consigo mismo con diferentes parámetros. De los inconvenientes, un tamaño de tarea muy limitado: para optimizar cien parámetros (un número completamente adecuado para el motor) no es posible, al menos durante un tiempo adecuado.Afinación de Texel
Resuelve el problema del método anterior. Tomamos una gran cantidad de posiciones (el autor ofreció 9 millones de posiciones de 64,000 juegos, saqué 8 millones de casi 200,000), por cada uno guardamos el resultado del juego (las blancas ganaron 1, empataron 0.5, derrotaron 0). Ahora minimizamos el error, que es la suma de los cuadrados de la diferencia del resultado y el sigmoide de la estimación. El método es efectivo y popular, pero no funciona en todos los motores.Afinación de stockfish
Otra técnica del líder. Tomamos un parámetro igual a x, y comparamos (en varias decenas de miles de lotes) el motor con un parámetro igual a x-sigma y x + sigma. Si el motor con un parámetro grande ganó, muévalo un poco hacia arriba; de lo contrario, baje un poco y repita.Competiciones de motores
De todas las pruebas de competencia realizadas, me gustaría distinguir por separado TCEC . Se diferencia de todos los demás en su poderoso hardware, cuidadosa selección de aberturas y largo control. En la última final, se jugaron 100 juegos en 2 x Intel Xeon E5-2690v3 con 256 gigabytes de RAM con control de 180 '+ 30 ". En tales condiciones, el número de sorteos fue enorme, y solo 11 juegos fueron efectivos.Conclusión
Entonces, tan brevemente en este largo artículo hablé aproximadamente sobre la disposición de los motores de ajedrez. Muchos detalles no fueron revelados, simplemente no sabía algo u olvidé decirlo. Si tiene alguna pregunta, escríbala en los comentarios. Además, le aconsejaré dos recursos que probablemente notó si abría cuidadosamente todos los enlaces dispersos a lo largo del artículo:Source: https://habr.com/ru/post/es390821/
All Articles