Brainstorm: como encarar tarefas de um ângulo diferente

Brainstorming com transposição


Às vezes, fico preso e tenho que procurar maneiras de pensar sobre a tarefa de um ângulo diferente. Existem tarefas que podem ser exibidas na forma de uma matriz ou tabela. A estrutura deles é mais ou menos assim:

UmBCDE
1A1B1C1D1E1
2A2B2C2D2E2
3A3B3C3D3E3
4A4B4C4D4E4
5A5B5C5D5E5

As células com as quais estou trabalhando são organizadas em colunas e linhas. Vamos dar um exemplo de um jogo simples:

AtaqueDefenderEspecial
Lutadorespadaarmadurabater
Magobola de fogorefletircongelar
Ladrãopunhalesquivardesarmar

Linhas são classes de personagens: guerreiro, mago, ladrão.

Colunas são tipos de ações: ataque, defesa, ação especial.

A matriz contém todo o código para processar cada um dos tipos de ações para cada tipo de caractere.

Como é o código? Normalmente, essas estruturas são organizadas em tais módulos:

  1. Fighter conterá um código para lidar com golpes de espada, reduzindo o dano com armadura e um golpe poderoso e especial.
  2. Mage conterá código de manuseio de bola de fogo, reflexão de dano e um ataque de congelamento especial.
  3. Thief conterá código para lidar com ataques de punhal, evitar danos de esquiva e um ataque desarmante especial.

Às vezes, é útil transpor a matriz. Podemos organizá-lo em outro eixo :

LutadorMagoLadrão
Ataqueespadabola de fogopunhal
Defenderarmadurarefletiresquivar
Especialbatercongelardesarmar

  1. Attack conterá um código para lidar com balanços, bolas de fogo e ataques de adagas.
  2. Defend conterá um código para lidar com a redução de danos, refletindo e evitando danos.
  3. Special conterá um poderoso código de manuseio, congelamento e desarmamento.

Foi-me ensinado que um estilo é "bom" e o outro é "ruim". Mas não me é óbvio por que tudo deveria ser assim. A razão é a suposição de que frequentemente adicionaremos novas classes de caracteres (substantivos) e raramente adicionaremos novos tipos de ações (verbos). Dessa forma, posso adicionar código usando o novo módulo sem tocar em todos os disponíveis. No seu jogo, tudo pode ser diferente. Olhando para a matriz transposta, estou ciente da existência da suposição e posso lançar dúvidas sobre ela. Depois, pensarei no tipo de flexibilidade de que preciso e só então selecionarei a estrutura do código.

Vejamos outro exemplo.

As interpretações das linguagens de programação possuem vários tipos de nós correspondentes às primitivas: constantes, operadores, loops, ramificações, funções, tipos, etc. Precisamos gerar código para todos eles.

Gerar código
Constante
Operador
Loop
Branch
Função
Tipo

Ótimo! Você pode criar uma classe para cada tipo de nó e todos eles podem herdar do Node classe base. Mas confiamos no pressuposto de que adicionaremos linhas e colunas com menos frequência. O que acontece no compilador de otimização? Estamos adicionando novos passes de otimização. E cada um deles é uma nova coluna.

Gerar códigoFluxo de dadosDobragem constanteFusão de loop...
Constante
Operador
Loop
Branch
Função
Tipo

Se eu quiser adicionar um novo passe de otimização, precisarei adicionar um novo método a cada classe, e todo o código de otimização será distribuído em diferentes módulos. Eu quero evitar essa situação! Portanto, em alguns sistemas, outra camada é adicionada sobre isso. Usando o padrão de visitante, posso armazenar todo o código para mesclar loops em um módulo, em vez de dividi-lo em vários arquivos.

Se você olhar para a matriz transposta, abriremos outra abordagem:

ConstanteOperadorLoopBranchFunçãoTipo
Gerar código
Fluxo de dados
Dobragem constante
SSA
Fusão de loop

Agora, em vez de classes com métodos, posso usar união marcada e correspondência de padrões (eles não são suportados em todas as linguagens de programação). Por esse motivo, todo o código de cada passagem de otimização será armazenado em conjunto e poderá ser dispensado da indireta do padrão de visitante.

Muitas vezes, é útil olhar para uma tarefa do ponto de vista da matriz. A aplicação a uma estrutura orientada a objetos na qual todos pensam pode me levar a outra coisa, como um padrão de entidade-componente-sistema, bancos de dados relacionais ou programação reativa.

E isso se aplica não apenas ao código. Aqui está um exemplo de aplicação dessa ideia aos produtos. Suponha que haja pessoas com interesses diferentes:

NickFengSayidAlice
carrosXX
políticaXX
matemáticaXX
viajarXX

Se eu estivesse desenvolvendo um site de rede social, poderia permitir que as pessoas seguissem as notícias de outras pessoas . Nick pode se inscrever na Alice, porque ambos estão interessados ​​em carros, e no Fenya, porque ambos estão interessados ​​em viajar. Mas Nick também receberá os posts de Alice em matemática e os de Fenya em política. Se eu estivesse considerando uma matriz transposta, poderia permitir que as pessoas se inscrevessem nos tópicos . Nick poderia se juntar a um grupo de amantes de carros, bem como a um grupo de viajantes. O Facebook e o Reddit começaram na mesma época, mas são matrizes transpostas uma da outra. O Facebook permite que você siga pessoas; O Reddit permite que você assine tópicos.

Quando eu paro ou quando quero considerar alternativas, olho para o problema e procuro por diferentes eixos de pedidos. Às vezes, olhar para um problema de um ângulo diferente pode fornecer uma solução melhor.

Brainstorming em decomposição


Eu uso outra técnica chamada decomposição.

Na álgebra, a operação de decomposição transforma um polinômio da forma 5x² + 8x - 21 em (x + 3) · (5x - 7). Para resolver a equação 5x² + 8x - 21 = 0, podemos primeiro fatorá-la em (x + 3) · (5x - 7) = 0. Então podemos dizer que x + 3 = 0 ou 5x - 7 = 0. Expansão transforma uma tarefa difícil em algumas tarefas mais fáceis.

x3
5x5x²15x
-7-7x-21

Vamos dar uma olhada em um exemplo: Eu tenho seis classes: File , EncryptedFile , GzipFile , EncryptedGzipFile , BzipFile , EncryptedBzipFile . Eu posso decompô-los em uma matriz:

DescomprimidoGzipBzip
Não criptografadoFicheiroGzip (arquivo)Bzip (arquivo)
CriptografadoCriptografar (arquivo)Criptografar (Gzip (arquivo))Criptografar (Bzip (arquivo))

Usando o padrão “decorador” (ou impurezas), transformei seis tipos diferentes de arquivos em quatro componentes: simples, gzip, bzip, criptografar. Isso não parece economizar muito, mas se eu adicionar mais variações, a economia aumentará. A decomposição transforma componentes O (M * N) em componentes O (M + N).

Outro exemplo: às vezes as pessoas me fazem perguntas como "como escrever interpolação linear em C #?" . Eu posso escrever muitos tutoriais em potencial:

C ++PythonJavaC #JavascriptFerrugemIdris...
Interpolação
Vizinhos
Pathfinding
Distâncias
Mapas fluviais
Isométrico
Voronoi
Transformações
...

Se houver M tópicos e N idiomas, posso escrever tutoriais M * N. No entanto, isso é muito trabalho. Em vez disso, vou escrever um tutorial sobre interpolação , alguém escreverá um tutorial sobre C # e, em seguida, o leitor combinará o conhecimento de C # com o conhecimento de interpolação e escreverá sua versão da interpolação em C #.

Como a transposição, a decomposição nem sempre ajuda, mas, se aplicável, pode ser bastante útil.

Brainstorming para trás


Nas duas partes anteriores, falei sobre como às vezes abordo a tarefa, tentando organizá-la em uma matriz. Às vezes isso não ajuda e, em seguida, tento olhar a tarefa na direção oposta. Vamos dar uma olhada na geração de mapas procedurais, por exemplo. Freqüentemente começo com uma função de ruído, adiciono oitavas, ajusto parâmetros e adiciono camadas. Faço isso porque preciso de cartões com certas propriedades.


É bem possível iniciar experimentos com parâmetros, mas o espaço para parâmetros é bastante grande e não se sabe se vou encontrar os parâmetros mais adequados para meus requisitos. Portanto, tendo experimentado um pouco, paro e começo a pensar na ordem inversa: se consigo descrever o que preciso, isso pode ajudar a encontrar os parâmetros.

Foi essa motivação que me fez estudar álgebra. Se temos uma equação da forma 5x² + 8x - 21 = 0 , então o que será x ? Quando não conhecia álgebra, resolvia essa equação, tentando substituir diferentes valores de x , primeiro escolhendo-os aleatoriamente e depois ajustando-os quando considero que estou perto da solução. A álgebra nos fornece uma ferramenta para ir em uma direção diferente. Em vez de adivinhar as respostas, ela me fornece um aparato (decomposição ou equações quadráticas, ou o método newtoniano de busca iterativa de raízes), que eu posso usar de maneira mais consciente para procurar valores x (-3 ou 7/5).

Sinto que frequentemente entro esse tipo de situação de programação. Enquanto trabalhava na geração de mapas procedurais, depois de experimentar os parâmetros por algum tempo, parei e compilei uma lista do que deveria estar nos mundos dos jogos de um projeto :

  1. Os jogadores devem começar o jogo longe da costa.
  2. Ao subir de nível, os jogadores devem subir morro acima.
  3. Os jogadores não devem conseguir alcançar a borda do mapa.
  4. À medida que o nível aumenta, os jogadores devem participar de grupos.
  5. Deveria haver monstros simples nas costas sem muita variação.
  6. Nas planícies, deve haver uma grande variedade de monstros de dificuldade média.
  7. Em áreas montanhosas, deve haver monstros chefes complexos.
  8. Deve haver algum tipo de marco que permita aos jogadores permanecer no mesmo nível de dificuldade e outro marco que permita que você suba ou desça no nível de dificuldade.

A compilação desta lista levou à criação das seguintes restrições:

  1. Os mundos dos jogos devem ser ilhas com muitas costas e um pequeno pico no centro.
  2. A altitude deve corresponder à complexidade dos monstros.
  3. Em altitudes baixas e altas, deve haver menos variabilidade de biomas do que em altitudes médias.
  4. As estradas devem permanecer no mesmo nível de dificuldade.
  5. Os rios devem fluir de grandes a pequenas alturas e fornecer aos jogadores a capacidade de subir / descer.

Essas limitações me levaram a projetar um gerador de mapas. E ele levou à geração de um conjunto de cartões muito melhor do que aqueles que eu recebi ajustando os parâmetros, como normalmente faço. E o artigo resultante interessou muitas pessoas na criação de mapas baseados nos diagramas de Voronoi.

Testes de unidade são outro exemplo. Sugere-se que eu apresente uma lista de exemplos para testar. Por exemplo, para grades de hexágonos, acho que preciso verificar a condição add(Hex(1, 2), Hex(3, 4)) == Hex(4, 6) . Então, lembro que você precisa verificar os zeros: add(Hex(0, 1), Hex(7, 9)) == Hex(7, 10) . Então, lembro que você precisa verificar valores negativos: add(Hex(-3, 4) + Hex(7, -8)) == Hex(4, -4) . Bem, ótimo, eu tenho alguns testes de unidade.

Mas se você pensar um pouco mais, na verdade eu verifico add(Hex(A, B), Hex(C, D)) == Hex(A+C, B+D) . Eu vim com os três exemplos mostrados acima com base nesta regra geral. Estou indo na direção oposta a esta regra, a fim de chegar a testes de unidade. Se eu puder codificar diretamente essa regra em um sistema de teste, o próprio sistema poderá trabalhar na ordem inversa para criar exemplos para teste. Isso é chamado de teste baseado em propriedade. (Veja também: teste metamórfico )

Outro exemplo: solucionadores de restrições. Nesses sistemas, o usuário descreve o que deseja ver na saída, e o sistema encontra uma maneira de satisfazer essas restrições. Citação do livro de geração de conteúdo processual, capítulo 8 :

Usando os métodos construtivos do Capítulo 3, bem como os métodos fractal e ruído do Capítulo 4, podemos criar vários tipos de dados de saída ajustando os algoritmos até estarmos satisfeitos com os dados de saída. Porém, se soubermos quais propriedades o conteúdo gerado deve ter, será mais conveniente indicar diretamente o que queremos que o algoritmo geral encontre conteúdo que atenda aos nossos critérios.

Este livro descreve a programação de conjuntos de respostas (ASP), na qual descrevemos a estrutura com a qual estamos trabalhando (ladrilhos são pisos e paredes, ladrilhos se confinam), a estrutura das soluções que estamos procurando (uma masmorra é um grupo telhas conectadas com o começo e o fim) e as propriedades das soluções (passagens laterais não devem conter mais de 5 salas, deve haver 1-2 voltas no labirinto, três assistentes devem ser derrotados antes de chegar ao chefe). Depois disso, o sistema cria soluções possíveis e permite que você decida o que fazer com elas.

Recentemente, um solucionador de restrições foi desenvolvido, o que causou grande interesse devido ao seu nome legal e curiosa demonstração: Wave Function Collapse. [Há um artigo sobre esse solucionador em Habr.] Se você der exemplos de imagens para indicar quais restrições são impostas aos ladrilhos vizinhos, ele criará novos exemplos que correspondem aos padrões fornecidos. Seu trabalho é descrito em WaveFunctionCollapse é Resolução de restrições na natureza :

O WFC implementa um método de pesquisa ganancioso sem voltar atrás. Este artigo explora o WFC como um exemplo de métodos de decisão baseados em restrições.

Já consegui muito com a ajuda de solucionadores de restrições. Como na álgebra, antes de aprender a usá-las de maneira eficaz, preciso aprender muito.

Outro exemplo: a nave espacial que criei . O jogador pode arrastar os motores para qualquer lugar e o sistema determinará quais motores precisam ser ativados quando você clicar em W, A, S, D, Q, E. Por exemplo, nesta nave:


Se você quiser voar para a frente, inclua dois motores traseiros. Se você quiser virar à esquerda, ligue os motores dianteiro direito e traseiro esquerdo. Tentei procurar uma solução, forçando o sistema a repetir muitos parâmetros :


O sistema funcionou, mas não é perfeito. Mais tarde, percebi que este é outro exemplo de onde a solução na direção oposta poderia ajudar. Descobriu-se que o movimento da espaçonave pode ser descrito por um sistema linear de restrições . Se eu entendi isso, poderia usar uma biblioteca pronta que resolva com precisão as restrições, e não meu método de tentativa e erro, que retorna uma aproximação.

E outro exemplo: o projeto G9.js., no qual você pode arrastar a saída de uma determinada função na tela, e determina como alterar os dados de entrada para corresponder aos dados de saída desejados. As demos do G9.js estão ótimas! Certifique-se de descomentar a linha "descomente a linha a seguir" na demonstração de Anéis.

Às vezes, é útil pensar em uma tarefa na ordem inversa. Muitas vezes acontece que isso me dá melhores soluções do que com o raciocínio direto.

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


All Articles