Sobre o imutável: história do 9º lugar da Copa da AI russa 2019

Chamo-me Andrey Rybalka, participo da Taça da AI da Rússia com o apelido de lama e direi novamente como não ganhar um macbook. Felizmente, sou uma pessoa experiente nisso - com essas mãos não ganhei até 7 peças.


Então, a tarefa deste ano foi um jogo de plataformas / 2D, para o qual você teve que escrever um bot. O jogo ficou assim:



O bot ficou assim:



Se você estiver interessado em saber como a figura 2 foi reproduzida na figura 1, por favor, abaixo do gato


Se você não participou e não leu outros artigos, recomendo que você veja primeiro como tudo se parece em dinâmica no site ou no tube:



Sistema de torneio


Para iniciantes, são fornecidas mais de 2 semanas para programação. Então a primeira rodada começa. Dura 2 dias e os 300 melhores continuam. Após a rodada, as regras do jogo mudam (agora controlamos dois personagens ao mesmo tempo) e outra semana é dada, após a qual a segunda rodada passa. Então as regras são complicadas novamente (agora jogamos em mapas muito mais complexos), outra semana é dada e, finalmente, jogamos a final.


Mas este não é o fim. Após as finais, há mais uma semana, no final da qual a caixa de areia simplesmente para, e as 6 melhores nela, excluindo os vencedores das finais, também são premiadas. A diferença fundamental entre o final da área restrita e a final do campeonato é que, na área restrita, os jogos são criados em um formato aleatório, e não apenas no formato da rodada atual.


Histórico de participação


Onde sem ele? Você pode pular quem não estiver interessado. A parte técnica será menor.


Semana do Teste Beta e Rodada Um


Comecei a programar, ao que parece, no dia seguinte ao início do Open Beta Test. Mas, pessoalmente, foi um pouco desmotivador para mim que os organizadores desta vez, ao contrário do passado, decidiram se afastar novamente da prática de publicar um pseudo-código de um simulador. Certamente entre os participantes estavam aqueles que gostam de escrever um simulador por engenharia reversa, mas eu não sou um deles e fiquei entediado com isso. Como este não foi meu primeiro campeonato, eu sabia que mais cedo ou mais tarde me envolveria, mas, pelo motivo descrito acima, me envolvi tarde demais. Como resultado, o que alguns participantes fizeram nos primeiros dias, fui capaz de me forçar a terminar em apenas uma semana e meia. Como resultado, escrevi a primeira linha de código do bot 5 dias antes da primeira rodada. Em resumo, no início do primeiro turno, eu não estava pronto e o turno passou sem a minha participação. Decidi entrar no segundo turno através da discagem de ramal.


Segunda rodada


Neste ponto, eu já programei ao máximo, em média 4-6 horas por dia. Alguns dias antes do início do segundo turno, enviei a primeira versão do bot. Ela imediatamente subiu rapidamente e entrou rapidamente nas 10 principais caixas de areia. Então a rodada começou, onde eu fiquei em quinto lugar.


Final


Passei a primeira noite da semana final (em quatro, porque estava marcado um início para sexta-feira) para encontrar o caminho. Ainda havia muitas idéias, mas o que me concentrar e qual das potencialmente daria o melhor resultado não estava claro, então tentei melhorar em primeiro lugar o que causou o maior número de derrotas. Na terça e na quarta-feira, houve melhorias na mira e no tiro, além do controle dos kits de primeiros socorros.


Então vi que algo havia acontecido que eu esperava há muito tempo, mas esperava que isso não acontecesse - alguns oponentes começaram a usar ativamente as minas.


O fato é que, durante o OBT, os organizadores ouviram a proposta dos participantes e mudaram uma das mecânicas do jogo. No caso geral, é claro que isso é bom quando os participantes são ouvidos. Mas, especificamente, desta vez, a presença de feedback fez uma piada cruel. Em resumo, os organizadores tornaram possível que as minas fossem detonadas com um tiro.


Isso por si só é lógico. Se eu fosse minha e eles atirassem em mim, eu também explodiria. Mas o problema é que jogamos em pontos, e os pontos foram dados não apenas por assassinatos, mas também por danos. Portanto, se você chegar perto o suficiente do inimigo, colocar minas sob você e atirar nelas, você mata o inimigo e a si mesmo com uma explosão. É impossível esquivar. Vocês dois recebem 1000 pontos pelos dois personagens que estão morrendo, mas também recebem pontos por danos. Assim, se o inimigo estiver saudável, você receberá 1000 pontos por matar e 100 por dano, e o inimigo - apenas 1000.


Embora todos soubessem disso, mas no topo quase ninguém usava minas até o último. Não sei como os outros, mas, pessoalmente, não o usei simplesmente porque não queria abusar do cálculo incorreto involuntário. Mas, como disseram em um filme, eu sabia que mais cedo ou mais tarde passaremos a esse lixo.


Em resumo, no meio da noite, de quarta a quinta-feira, eu agitei a auto-interrupção. Puramente situacional, nada avançado. Apenas pelo princípio de que, se no momento atual, meu personagem é rentável para se matar, e ele pode fazê-lo, ele faz. Como a prática demonstrou, alguns outros participantes planejaram usar as minas com antecedência, por isso implementaram um suicídio mais avançado, mas planejaram estrategicamente liberá-lo imediatamente antes da final. Como resultado, aconteceu o que aconteceu - 9º lugar na final, assim como no ano passado.


Sandbox Finale


Após o final, eu deixei a cidade para as férias. Ele voltou uma semana depois e, na primeira noite, literalmente, em 2 a 3 horas de trabalho, melhorou muito as minas, sobre as quais vou escrever mais na parte técnica. O restante das alterações dizia respeito principalmente à edição de bugs e à conclusão da função de avaliação. Em resumo, um pouco de programação e muitos testes.


Parte técnica


A velocidade do Java ou o raio de curvatura das mãos, mas os dois juntos, mais uma vez não me permitiram me dar bem com uma simulação pura. Portanto, o movimento, o disparo normal e a instalação de minas são separados de mim.


Simulação


Pelos olhos de um bot, o mundo era assim:



No vídeo, eu acho, e então tudo está aproximadamente claro. No canto superior esquerdo, está a depuração da função de avaliação. Você pode ver em que consiste a pontuação de cada trajetória. Silhuetas amarelas (por exemplo, às 2:30) - essas são as mesmas trajetórias em 9 direções, sobre elas serão mais baixas. Linhas de flechas em algum ponto das 2:50 são uma maneira de procurar (vermelho - para o inimigo, verde - para o kit de primeiros socorros, verde-amarelo - para a arma, azul - para a mina). Os quadrados verdes no final do vídeo são meus blocos PVS, que são visíveis no bloco selecionado. Os pontos vermelhos no centro indicam de onde vem a visibilidade reversa.


Movimentos de caracteres simulam sem microtics. Os vôos de bala também, mas neles eu uso vários hacks para que as situações ocorram menos quando o marcador "atravessa" o personagem sem tocá-lo, embora devesse ter tocado com microtics. Por exemplo, se você pensar bem, a situação descrita pode ocorrer apenas nos cantos:



No ponto 1, o marcador está na posição 1, no ponto 2 - na posição 2, respectivamente. Sem Mikrotik - Boris-rábano - você consegue, com Mikrotik - tiro na orelha. Portanto, para que isso aconteça, a posição do marcador nos pontos 1 e 2 deve estar em lados diferentes de pelo menos uma das faces do personagem ou lado a lado (um retângulo de hortelã). Assim, é possível simular uma bala sem microtics até que, conforme aplicada na figura acima, a condição old_bullet.x > character_left_side.x != new_bullet.x > character_left_side.x e, se isso ocorrer, analise com mais cuidado .


Além disso, cada carrapato recontava as trajetórias de todas as balas voadoras antes de colidirem com a parede e salvavam sua posição em cada carrapato em uma matriz, para que na simulação você pudesse verificar rapidamente colisões com elas.


Para uma bazuca, depois de bater em uma parede, eu matematicamente, sem microtics, calculei o ponto exato do impacto para calcular corretamente o epicentro da explosão.


Além disso, todos os dodge_trajectories que preenchi a matriz dodge_trajectories - simulavam o movimento de cada personagem, incluindo inimigos, em 8 direções para 25 ticks (silhuetas amarelas no vídeo do visualizador, depois de ativar a caixa de seleção de trajetória possível. Por exemplo, às 2:30). E, como nas balas, ele mantinha todas as posições possíveis em cada marca. Foi então usado em muitos lugares, alguns dos quais mencionarei.


Também calculei o PVS por blocos com antecedência. Para cada célula, eu mantinha uma lista de peças, cujo centro pode ser visto em pé nela. Foi calculado por traçado de raios. Isso pode ser visto no vídeo do visualizador, no final. Quadrados verdes são blocos que são visíveis na célula selecionada. Os pontos vermelhos no centro indicam de onde vem a visibilidade reversa.


Procure uma maneira


Implementado pelo algoritmo de Dijkstra de acordo com os Waypoints. O bloco em que você pode ficar foi considerado Waypoint. A adaptação do algoritmo para o jogo de plataformas 2D é própria, doméstica e, portanto, por uma questão de otimização, é construída sobre muletas. Mas funcionou rápido o suficiente: eu criei o Dijkstra anteriormente (não sabia como dizê-lo corretamente) de cada assunto no nível de cada bloco. Eu construí estradas apenas aquelas que têm capacidade de mão dupla nos dois países. Isso foi necessário para que mais tarde você pudesse passar rapidamente de qualquer ladrilho para qualquer kit de primeiros socorros / arma / mina. Era possível se livrar dessa restrição, mas, na prática, considerava mais razoável dedicar tempo a outras coisas, porque não havia muito dano com essa restrição.


Além disso, cada vez que troquei para outro ladrilho com qualquer personagem (meu e do inimigo), contei para ele o caminho para o inimigo mais próximo, o kit de primeiros socorros e também para a mina e as armas, se ele ainda precisasse delas.


No meu computador doméstico, em um jogo com 1000 ticks, toda a pesquisa de caminho levou cerca de 100 ms no total.


Se o caminho cruza um inimigo ou amigo, simplesmente adiciono algumas dúzias de unidades ao seu peso. Portanto, se houver um desvio relativamente curto, ou se, depois disso, for mais lucrativo correr para outro objeto, eu o farei. No vídeo do visualizador acima, isso pode ser visto às 2:55, onde o caminho para o inimigo mais próximo foi desviado, porque um caminho reto cruza o segundo caractere.


Visualização do caminho de pesquisa:



Quadrados roxos translúcidos são pontos de passagem, também são os vértices do gráfico. Setas verde limão - arestas 1 coluna 2
1 não deve ser confundido com costelas; 2 quaisquer coincidências de natureza monárquica são aleatórias [aprox.]


Movimento


Dois personagens caminharam por sua vez. Se você apenas pensou - "e agora chegou ao top 10?!", Então pensou corretamente. :) Não havia recursos suficientes para andar com os dois. Uma exceção foi o caso do aparecimento de uma nova bala voadora.


A base para a escolha de uma trajetória é a genética sem cruzamento. Mas se no ano passado, no futebol, a genética se mostrou perfeitamente, então este ano deu uma vantagem muito pequena. Minhas experiências mostraram um resultado muito próximo, tanto em genética quanto em pesquisas aleatórias. Vejo várias razões para isso, mas a principal me parece a seguinte: No futebol, tínhamos um objetivo fixo - a bola. Na maioria das vezes, seu comportamento era previsível - se encontrarmos uma boa trajetória para acertar a bola no gol e segui-la, depois de 20 tiques, essa trajetória provavelmente ainda será boa, porque a bola não pode mudar espontaneamente seu caminho. Mas este ano jogamos não com a bola, mas com personagens inimigos. Eles mudavam seu comportamento a cada carrapato. Portanto, a relevância da trajetória permaneceu muito curta.


Eu ainda usava genética por dois motivos:


  1. Acabei de copiar o código dela do ano passado. Foi até engraçado o quão pouco eu tive que fazer alterações, com exceção da função de avaliação, para que o bot começasse a se mover de forma tolerável. Resumindo, escrevi um simulador por uma semana e meia e, em seguida, copiei a lógica do movimento do ano passado, em outra hora, fiz uma estimativa simples e o bot, ao disparar no Quick Start, começou a ganhar bastante com ele.
  2. Eu aparentemente amo a dor. Caso contrário, não sei explicar por que escrevi em Java novamente, e não como todos os tops normais:


Então eu tive que fazer tudo de todas as maneiras para que a estratégia simplesmente não caísse com um tempo limite em cada primeiro jogo. E a abordagem com a genética me permitiu salvar um instantâneo da simulação do mundo e os pontos contados e, ao calcular as próximas gerações, desativar os instantâneos apenas a partir do primeiro gene mutado .


I.e. grosso modo, se na primeira geração eu der 10 ticks para a direita e depois pular, e na segunda geração avaliar um mutante que vai 10 ticks para a direita e depois pular, o ponto de referência para o mutante será o instantâneo calculado anteriormente do mundo e os pontos após 10 movimentos para a direita. Assim, reduzi significativamente o cálculo.


Igogo, um algoritmo de movimento aproximado:


  1. Geramos N genótipos aleatórios. Cada um consiste em M genes aleatórios. Cada gene é um número codificado em ação: um número é responsável pela direção do movimento, o segundo é para pular / caminhar / pular, o terceiro é para o disparo básico (puramente para a função de avaliação, o algoritmo básico do disparo será descrito abaixo), o quarto para o número de repetições deste ação. O número total de ações no genótipo, juntamente com repetições, não excede a profundidade da simulação - 40 ticks.
  2. Nós adicionamos vários genótipos codificados a eles: movimento direto em 9 direções (8 lados + parado) e algumas predefinições simples, que na prática ajudaram a sair de algumas situações típicas no labirinto um pouco mais rápido. Por exemplo, estas são as trajetórias: ⮤ ⮥
  3. Adicione o melhor genótipo da jogada anterior.
  4. Avalie tudo, deixe o M melhor.
  5. Estamos criando a próxima geração em que, em cada genótipo, um ou mais genes sofreram mutações.
  6. Nós os adicionamos ao pool geral, que já contém o M best da última geração.
  7. Repetimos, começando no ponto 4, várias vezes.

Função de avaliação


Na verdade, o lugar onde a estratégia vive.


Ao longo do campeonato, um monte de todos os tipos de métricas foi adicionado (para ser preciso, 57). Alguns deles não viveram para assistir às finais. A outra parte sobreviveu, mas no contexto da inflação do Score durante o campeonato, como resultado, praticamente não afetou o resultado, mas o restante, da ordem de 20 a 25, foi precisamente responsável pelo movimento e tiro básico.


Vou dar alguns exemplos de métricas importantes, em ordem aleatória:


  1. Pena para uma explosão de bala / bazuca em mim.
  2. Bônus por graus de liberdade. I.e. para o número de direções (para cima / baixo / esquerda / direita) onde eu posso mover do bloco atual. Como você pode imaginar, quanto mais graus de liberdade - mais chances de desviar de uma bala. Esse bônus obrigou o bot a aderir a plataformas e escadas. O bônus é três vezes maior se o oponente tiver uma bazuca.
  3. Penalidade pela distância (através da busca pelo caminho) até o kit de primeiros socorros mais próximo; pela distância média (através da busca pelo caminho) a todos os kits de primeiros socorros; porque o caminho dele até o kit de primeiros socorros mais próximo dele é menor do que o caminho de mim até o (!) kit de primeiros socorros.
  4. Pena pelo fato de o inimigo ver minha cabeça.
  5. Pena pelo fato de o inimigo ver minhas pernas (os pontos 4 e 5 forçaram o bot a se esconder atrás de abrigos parciais).
  6. Ataque bônus enquanto recarrega as armas inimigas.
  7. Bônus por tiros feitos.
  8. Penalidade por muita ou pouca distância do inimigo. Dependia de vários fatores: a saúde de ambos, se as armas de ambos foram recarregadas, etc.
  9. Bônus por condições perigosas de trabalho.
  10. Penalidades pelo estado de queda e pelo estado de voo no jumppad (porque esses dois estados não podem ser interrompidos e, portanto, são muito menos propensos a evitar a bala).
  11. O bônus pelo número de kits de primeiros socorros que estão do meu lado oposto, comparado com o inimigo (por exemplo, se o inimigo está à minha direita, receberei um bônus pelo número de kits de primeiros socorros à minha esquerda. Isso basicamente fazia sentido em Rodadas 1 e 2, onde o único caminho do inimigo para esses kits de primeiros socorros era através de mim).
  12. Bônus por seguir o caminho para o inimigo, para o kit de primeiros socorros (se estou ferido), para uma mina (se tiver menos de dois), para armas (se não houver).
  13. Penalidade pela proximidade das muralhas, caso o inimigo tenha uma bazuca. Mais especificamente, considerei quantos carrapatos o foguete do inimigo, se ele o soltar, voará para a parede perto de mim e, com base nisso, aquela zona perto da parede da qual é impossível escapar da explosão era considerada perigosa e recebi uma multa por estar nela.
  14. Bônus de ano novo.

Evasão à bala


Isso acontece automaticamente, devido ao acima


Visando


Há muita coisa acontecendo. Talvez o mais importante seja um algoritmo simples e eficaz que decida se devo apontar uma arma para o inimigo (porque apontar aumenta a propagação de armas):


  1. Consideramos o ângulo entre (last_angle - min_spread) e (last_angle + max_spread).
  2. Atiramos os raios do centro do meu personagem para os dois cantos da AABB inimiga mais próxima da perpendicular. Se algum deles estiver fora do intervalo (last_angle - min_spread) ... (last_angle + max_spread), cortamos esse intervalo.
  3. Consideramos o ângulo entre esses raios.
  4. Divida a primeira segunda esquina pela primeira, obtemos Cobertura (cobertura). Representa a probabilidade atual em porcentagem de que a trajetória da bala disparada cruze com a caixa do oponente.
  5. Simulamos uma Ação em que ocorre um inimigo, juntamente com uma mudança na dispersão.
  6. Repita as etapas 1..4 para o novo conjunto [last_angle, min_spread, max_spread]. Assim, consideramos qual será a cobertura caso pretendamos.
  7. Como resultado, temos a cobertura atual, bem como a cobertura prevista, caso apontemos a arma para o inimigo. Se a cobertura estimada for maior que a atual, pretendemos.

Demonstração 2 pontos:



Se as linhas são verdes, o inimigo cai inteiramente na área de mira. Laranja - parcialmente vermelho - não cai.


Mas eu geralmente não viso a barriga do inimigo, mas em algum ponto mais ideal.


Para uma pistola e uma espingarda de assalto, se o inimigo estiver no ar, considero quantos tiques a bala alcançará sua posição atual, então dodge_trajectories sua matriz dodge_trajectories descrita acima de todas as suas posições possíveis através desse número de tiques, média todas as 8 posições e, ao apontar, considero que o inimigo está nesse ponto médio.


Se ele estiver no chão, aponto "na cabeça", com base nessas considerações de que ele só pode pular para cima, de modo que um tiro na cabeça seja muito mais difícil de se esquivar do que um tiro no estômago e, mais ainda, nas pernas.


Para uma bazuca, eu também tomo 8 posições possíveis em tantos carrapatos quanto o foguete atinge o inimigo. Para eles, construo um cone (mais precisamente, um setor de um círculo) que descreve todas essas posições e, dentro desse cone, separo as trajetórias com algum passo. Para cada trajetória, eu simulo um vôo de foguete, juntamente com uma explosão no caso de um golpe na parede. A trajetória em que o inimigo tem menos chance de desviar, eu uso para mirar.


Mesmo em alguns casos, para cair e voar do jumppad (ou seja, aqueles estados que o inimigo não pode interromper), simplesmente considero o ponto de interceptação pela bala do inimigo, resolvendo o sistema de equações de movimento. O código foi copiado de seu próprio bot de hóquei de 2014, onde foi usado para interceptar o disco. :)


Entre outras coisas, em algumas situações cancelei a mira se houver uma posição no meu caminho para os próximos 10 tiques em que o raio do centro dessa posição na direção do último triângulo final se cruza com o inimigo. Isso me permitiu mirar com o movimento, sem alterar o ângulo e, portanto, sem aumentar a propagação.


Tiro


O disparo básico foi costurado na trajetória encontrada e era puramente situacional. Mas também, todo tick que meu bot tentou calcular se faz sentido disparar agora e, se o fez, disparou. Por exemplo:


  • se não há garantia de que o inimigo se esquive;
  • se a cobertura estava acima de algum valor fixo.

Além disso, se minha arma era uma bazuca, simulei todos os tipos de trajetórias dentro do spread no caso de um tiro imediato, com algum passo. E se o resultado mostrou que o benefício potencial excede o dano potencial, eu atirei. Avaliei o benefício em 4 métricas em ordem decrescente de importância para mim e para o inimigo: morte garantida (minha ou inimiga), morte possível, dano garantido, dano possível. Se havia pelo menos uma trajetória na qual, por exemplo, eu tinha a garantia de me matar e possivelmente matar o inimigo, não atirei.


Houve outros cheques. Por exemplo, eu poderia cancelar um tiro se no próximo tick da minha trajetória a cobertura potencial for maior do que na atual.


Em geral, a lógica me dizia que o disparo básico deveria ser desativado, mas os testes mostraram o contrário. Não consigo imaginar o porquê.


Minas


No momento da final, eles estavam na infância: marquei cada marca o que aconteceria se eu colocasse 1 ou 2 minas agora, dependendo da saúde do oponente, e atire nelas. Se eu tenho a garantia de matar o inimigo e se é rentável para mim (em pontos ou porque apenas o inimigo vai morrer), eu o fiz.


Após o ano novo, voltando de uma viagem, escrevi o algoritmo na primeira noite, o que me levou a 2-4 lugares na tabela. O algoritmo era simples: eu apenas simulei cada tick o que aconteceria se eu mudasse imediatamente para o modo furioso e corresse em direção ao inimigo para explodi-lo com uma mina o mais rápido possível. Para o inimigo, simulei uma evasão simples usando os mesmos dodge_trajectories: fiz três trajetórias que aumentaram a distância de mim. Por exemplo, se eu estivesse à esquerda do inimigo, analisei três casos: o inimigo foge para a direita; o inimigo corre para a direita e pula; corre para a direita e pula. Se, nos três casos, eu tinha garantia de ter tempo para matá-lo com uma mina e ele não podia colocar minas antes de mim, eu o fazia.


Além disso, o algoritmo foi capaz de pular ou pular no nível da estratégia de Início Rápido - simplesmente com base na diferença na coordenada Y.


Como resultado, nos mapas finais, quase todas as partidas terminavam em menos de 1000 tiques com explosões automáticas de meus dois personagens.


Pacifismo


Vendo como minhas minas eram eficazes, decidi verificar no meu telegrama minha suposição anterior de que na final era possível ocupar um lugar no topo sem disparar. Então, eu apenas tirei e desliguei o tiro no meu bot, com exceção de um tiro na mina. A única coisa é - para não perder a classificação, este modo era ativado apenas se eu tivesse minas suficientes para matar o inimigo.


Em resumo, meu bot se tornou pacifista. Para demonstrar sua paz e humildade, ele nem olhou para o inimigo (veja o vídeo). Bem, às vezes ele atirava em minas, você pensa. Eu não atirei nos oponentes! E se eles morreram ao mesmo tempo - bem, o que você pode fazer, um trágico acidente.


Em geral, enviei esta versão para o site, criei 4 jogos com cada um dos três primeiros e ...



... venceu 3 de 4 jogos contra cada um. Em geral, é estranho que os vizinhos não tenham vindo reclamar da minha risada homérica no meio da noite. :)


Admire-se:



Eh, se eu passasse essas 2 ou 3 horas antes da final, e não depois, talvez este artigo dissesse como ganhar um MacBook, e não como evitá-lo de qualquer maneira, quem sabe. Infelizmente, a história não conhece o humor subjuntivo.


Teste


Em geral, testei todas as alterações tantas vezes que o resultado foi estatisticamente significativo. Mais precisamente, o limite inferior do intervalo de confiança deve exceder 50%. Para as finais da caixa de areia, o script selecionou coeficientes. Nos anos anteriores, tentei a genética, mas funcionou mal - para um resultado normal, você tinha que jogar muitos jogos. Desta vez, apenas alterei um coeficiente de cada vez e recuperei 200 jogos. Nos casos em que o resultado foi bom, realizei testes adicionais. Em suma, deixou durante a noite duas dúzias de coeficientes e teve um resultado pela manhã.


Em conclusão


De alguma forma, tudo isso funcionou com o sofrimento pela metade.


Nos últimos dias antes do final do sandbox, passei a maior parte do tempo em segundo lugar na tabela por uma margem significativa em relação ao terceiro, mas algumas horas antes do final do sandbox fiquei sem sorte e comecei uma série de jogos de acordo com as regras das rodadas 1 e 2, enquanto minha estratégia jogou mais pelas regras do final. Por exemplo, aqui de 13 jogos, há apenas 1 de acordo com as regras finais. Então eu também, minha taxa de vitórias nesses jogos saiu muito pior que o normal. Em geral, o todo-poderoso aleatório:



Como resultado, perdi muitos jogos, perdi todas as minhas vantagens e caí de 2 para 4 lugares e não tive tempo de me recuperar, porque o campeonato havia terminado. É bom que a sandbox não tenha perdido o primeiro lugar na lista de vencedores.


Mais uma vez, quero agradecer ao Mail.ru Group por este próximo campeonato maravilhoso. , , , ( 1100 ). — , . , . , , , !


, . , , . " 8 ".

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


All Articles