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:
As células com as quais estou trabalhando são organizadas em colunas e linhas. Vamos dar um exemplo de um jogo simples:
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:
Fighter
conterá um código para lidar com golpes de espada, reduzindo o dano com armadura e um golpe poderoso e especial.Mage
conterá código de manuseio de bola de fogo, reflexão de dano e um ataque de congelamento especial.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
:
Attack
conterá um código para lidar com balanços, bolas de fogo e ataques de adagas.Defend
conterá um código para lidar com a redução de danos, refletindo e evitando danos.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.
Ó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.
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:
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:
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.
Vamos dar uma olhada em um exemplo: Eu tenho seis classes:
File
,
EncryptedFile
,
GzipFile
,
EncryptedGzipFile
,
BzipFile
,
EncryptedBzipFile
. Eu posso decompô-los em uma matriz:
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:
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 :
- Os jogadores devem começar o jogo longe da costa.
- Ao subir de nível, os jogadores devem subir morro acima.
- Os jogadores não devem conseguir alcançar a borda do mapa.
- À medida que o nível aumenta, os jogadores devem participar de grupos.
- Deveria haver monstros simples nas costas sem muita variação.
- Nas planícies, deve haver uma grande variedade de monstros de dificuldade média.
- Em áreas montanhosas, deve haver monstros chefes complexos.
- 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:
- Os mundos dos jogos devem ser ilhas com muitas costas e um pequeno pico no centro.
- A altitude deve corresponder à complexidade dos monstros.
- Em altitudes baixas e altas, deve haver menos variabilidade de biomas do que em altitudes médias.
- As estradas devem permanecer no mesmo nível de dificuldade.
- 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.