Lluvia de ideas: cómo ver las tareas desde un ángulo diferente

Lluvia de ideas con transposición


A veces me atoro y tengo que buscar maneras de pensar sobre la tarea desde un ángulo diferente. Hay tareas que se pueden mostrar en forma de matriz o tabla. Su estructura se parece a esto:

UnBCDE
1A1B1C1D1E1
2A2B2C2D2E2
3A3B3C3D3E3
4 4A4B4C4D4E4
5 5A5B5C5D5E5

Las celdas con las que estoy trabajando están organizadas en columnas y filas. Tomemos un ejemplo de un juego simple:

AtacarDefenderEspecial
Luchadorespadaarmaduragolpe
Magobola de fuegoreflexionarcongelar
Ladrondagaesquivardesarmar

Las líneas son clases de personajes: guerrero, mago, ladrón.

Las columnas son tipos de acciones: ataque, defensa, acción especial.

La matriz contiene todo el código para procesar cada tipo de acción para cada tipo de personaje.

¿Cómo se ve el código? Típicamente, tales estructuras están organizadas en tales módulos:

  1. Fighter contendrá un código para manejar golpes de espada, reducir el daño con armadura y un poderoso golpe especial.
  2. Mage contendrá código de manejo de bola de fuego, reflejo de daño y un ataque de congelación especial.
  3. Thief contendrá código para manejar ataques de daga, evitar daños de esquiva y un ataque especial de desarme.

A veces es útil transponer la matriz. Podemos organizarlo en otro eje :

LuchadorMagoLadron
Atacarespadabola de fuegodaga
Defenderarmadurareflexionaresquivar
Especialgolpecongelardesarmar

  1. Attack contendrá un código para manejar columpios, bolas de fuego y ataques con dagas.
  2. Defend contendrá un código para manejar la reducción de daños, reflejar el daño y evitar daños.
  3. Special contendrá un poderoso código de manejo de ataque, congelación y desarmado.

Me enseñaron que un estilo es "bueno" y el otro es "malo". Pero no es obvio para mí por qué todo debería ser así. La razón es la suposición de que a menudo agregaremos nuevas clases de caracteres (sustantivos), y rara vez agregaremos nuevos tipos de acciones (verbos). De esta manera puedo agregar código usando el nuevo módulo sin tocar todos los disponibles. En tu juego, todo puede ser diferente. Al observar la matriz transpuesta, soy consciente de la existencia de la suposición y puedo ponerla en duda. Luego pensaré en el tipo de flexibilidad que necesito, y solo entonces elegiré la estructura del código.

Veamos otro ejemplo.

Las interpretaciones de los lenguajes de programación tienen varios tipos de nodos correspondientes a primitivas: constantes, operadores, bucles, ramificaciones, funciones, tipos, etc. Necesitamos generar código para todos ellos.

Generar código
Constante
Operador
Bucle
Rama
Función
Tipo

Genial Puede crear una clase para cada tipo de nodo, y todos pueden heredar del Node clase base. Pero confiamos en el supuesto de que agregaremos filas y menos frecuentemente columnas. ¿Qué sucede en el compilador de optimización? Estamos agregando nuevos pases de optimización. Y cada uno de ellos es una nueva columna.

Generar códigoFlujo de datosPlegado constanteFusión en bucle...
Constante
Operador
Bucle
Rama
Función
Tipo

Si deseo agregar un nuevo pase de optimización, necesitaré agregar un nuevo método a cada clase, y todo el código de pase de optimización se distribuirá en diferentes módulos. ¡Quiero evitar tal situación! Por lo tanto, en algunos sistemas, se agrega otra capa además de esto. Utilizando el patrón de visitante, puedo almacenar todo el código para fusionar bucles en un módulo, en lugar de dividirlo en varios archivos.

Si observa la matriz transpuesta, abriremos otro enfoque:

ConstanteOperadorBucleRamaFunciónTipo
Generar código
Flujo de datos
Plegado constante
SSA
Fusión en bucle

Ahora, en lugar de clases con métodos, puedo usar la unión etiquetada y la coincidencia de patrones (no son compatibles con todos los lenguajes de programación). Debido a esto, el código completo de cada pase de optimización se almacenará en conjunto y puede prescindir de la indirecta del patrón del visitante.

A menudo es útil mirar el problema desde el punto de vista de la matriz. Si lo aplica a una estructura orientada a objetos en la que todos piensan, puede llevarme a algo más, por ejemplo, al patrón "sistema de entidad-componente", bases de datos relacionales o programación reactiva.

Y esto se aplica no solo al código. Aquí hay un ejemplo de cómo aplicar esta idea a los productos. Supongamos que hay personas con intereses diferentes:

NickFengSayidAlice
carrosXX
politicaXX
matematicasXX
viajarXX

Si estuviera desarrollando un sitio de redes sociales, podría permitir que la gente siguiera las noticias de otras personas . Nick puede inscribirse en Alice, porque ambos están interesados ​​en los autos, y en Fenya, porque ambos están interesados ​​en viajar. Pero Nick también recibirá las publicaciones de Alice sobre matemáticas y las publicaciones de Fenya sobre política. Si estuviera considerando una matriz transpuesta, podría permitir que las personas se suscriban a los temas . Nick podría unirse a un grupo de amantes del automóvil, así como a un grupo de viajeros. Facebook y Reddit comenzaron aproximadamente al mismo tiempo, pero son matrices transpuestas entre sí. Facebook te permite seguir personas; Reddit le permite suscribirse a temas.

Cuando me detengo o cuando quiero considerar alternativas, miro el problema y busco diferentes ejes de orden en él. A veces, mirar un problema desde un ángulo diferente puede proporcionar una mejor solución.

Lluvia de ideas de descomposición


Yo uso otra técnica llamada descomposición.

En álgebra, la operación de descomposición transforma un polinomio de la forma 5x² + 8x - 21 en (x + 3) · (5x - 7). Para resolver la ecuación 5x² + 8x - 21 = 0, primero podemos factorizarla en (x + 3) · (5x - 7) = 0. Luego podemos decir que x + 3 = 0 o 5x - 7 = 0. Expansión convierte una tarea difícil en algunas tareas más fáciles.

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

Veamos un ejemplo: tengo seis clases: File , EncryptedFile , GzipFile , EncryptedGzipFile , BzipFile , EncryptedBzipFile . Puedo descomponerlos en una matriz:

Sin comprimirGzipBzip
Sin cifrarArchivoGzip (archivo)Bzip (archivo)
EncriptadoCifrar (archivo)Cifrar (Gzip (Archivo))Cifrar (Bzip (Archivo))

Usando el patrón "decorador" (o impurezas), convertí seis tipos diferentes de archivos en cuatro componentes: simple, gzip, bzip, encriptado. Esto no parece ahorrar mucho, pero si agrego más variaciones, los ahorros aumentarán. La descomposición convierte los componentes O (M * N) en componentes O (M + N).

Otro ejemplo: a veces la gente me hace preguntas como "¿cómo escribir interpolación lineal en C #?" . Puedo escribir muchos tutoriales potenciales:

C ++PitónJavaC #JavascriptHerrumbreIdris...
Interpolación
Vecinos
Pathfinding
Distancias
Mapas del río
Isométrica
Voronoi
Transforma
...

Si hay M temas y N idiomas, entonces puedo escribir tutoriales M * N. Sin embargo, esto es mucho trabajo. En cambio, escribiré un tutorial sobre interpolación , alguien más escribirá un tutorial sobre C #, y luego el lector combinará el conocimiento de C # con el conocimiento de interpolación, y escribirá su versión de interpolación en C #.

Al igual que la transposición, la descomposición no siempre ayuda, pero si corresponde, puede ser bastante útil.

Lluvia de ideas hacia atrás


En las dos partes anteriores, hablé sobre cómo a veces abordo la tarea, tratando de organizarla en una matriz. A veces esto no ayuda y luego trato de mirar la tarea en la dirección opuesta. Echemos un vistazo a la generación de mapas de procedimientos, por ejemplo. A menudo empiezo con una función de ruido, luego agrego octavas, ajusto parámetros y agrego capas. Hago esto porque necesito tarjetas que tienen ciertas propiedades.


Es bastante posible comenzar con experimentos con parámetros, pero el espacio de parámetros es bastante grande, y no se sabe si encontraré los parámetros que más se ajusten a mis requisitos. Por lo tanto, después de experimentar un poco, me detengo y empiezo a pensar en el orden inverso: si puedo describir lo que necesito, esto puede ayudar a encontrar los parámetros.

Fue esta motivación la que me hizo estudiar álgebra. Si tenemos una ecuación de la forma 5x² + 8x - 21 = 0 , ¿cuál será x ? Cuando no conocía el álgebra, resolvía esta ecuación, tratando de sustituir diferentes valores de x , primero eligiéndolos al azar y luego ajustándolos cuando siento que me he acercado a la solución. El álgebra nos da una herramienta para ir en una dirección diferente. En lugar de adivinar las respuestas, me da un aparato (descomposición, o ecuaciones cuadráticas, o el método newtoniano de búsqueda iterativa de raíces), que puedo usar más conscientemente para buscar valores de x (-3 o 7/5).

Siento que a menudo me meto en este tipo de situación de programación. Mientras trabajaba en la generación de mapas de procedimientos, después de experimentar con los parámetros durante un tiempo, paré y compilé una lista de lo que debería estar en los mundos de juego de un proyecto :

  1. Los jugadores deben comenzar el juego lejos de la costa.
  2. Al subir de nivel, los jugadores deben subir cuesta arriba.
  3. Los jugadores no deberían poder alcanzar el borde del mapa.
  4. A medida que crece el nivel, los jugadores deben unirse en grupos.
  5. Debería haber monstruos simples en las costas sin mucha variación.
  6. En las llanuras debe haber una gran variedad de monstruos de dificultad media.
  7. En las zonas montañosas debe haber monstruos jefes complejos.
  8. Debe haber algún tipo de punto de referencia que permita a los jugadores permanecer en el mismo nivel de dificultad, y otro punto de referencia que le permita subir o bajar en el nivel de dificultad.

La compilación de esta lista condujo a la creación de las siguientes restricciones:

  1. Los mundos de juego deberían ser islas con muchas costas y un pequeño pico en el centro.
  2. La altitud debe corresponder a la complejidad de los monstruos.
  3. En altitudes bajas y altas, debería haber menos variabilidad de biomas que en altitudes medias.
  4. Los caminos deben permanecer en el mismo nivel de dificultad.
  5. Los ríos deben fluir de grandes a pequeñas alturas, y proporcionar a los jugadores la capacidad de moverse hacia arriba / abajo.

Estas limitaciones me llevaron a diseñar un generador de mapas. Y condujo a la generación de un conjunto de tarjetas mucho mejor que las que recibí ajustando los parámetros, como suelo hacer. Y el artículo resultante interesó a muchas personas en la creación de mapas basados ​​en diagramas de Voronoi.

Las pruebas unitarias son otro ejemplo. Se sugiere que se me ocurra una lista de ejemplos para probar. Por ejemplo, para cuadrículas de hexágonos, podría pensar que necesito verificar la condición add(Hex(1, 2), Hex(3, 4)) == Hex(4, 6) . Entonces puedo recordar que necesita verificar los ceros: add(Hex(0, 1), Hex(7, 9)) == Hex(7, 10) . Entonces puedo recordar que debe verificar los valores negativos: add(Hex(-3, 4) + Hex(7, -8)) == Hex(4, -4) . Bueno, genial, tengo algunas pruebas unitarias.

Pero si piensas un poco más, entonces de hecho verifico add(Hex(A, B), Hex(C, D)) == Hex(A+C, B+D) . Se me ocurrieron los tres ejemplos que se muestran arriba basados ​​en esta regla general. Voy en la dirección opuesta a esta regla para llegar a las pruebas unitarias. Si puedo codificar directamente esta regla en un sistema de prueba, entonces el sistema mismo podrá funcionar en orden inverso para crear ejemplos para pruebas. Esto se llama prueba basada en la propiedad. (Ver también: prueba metamórfica )

Otro ejemplo: solucionadores de restricciones. En tales sistemas, el usuario describe lo que quiere ver en la salida, y el sistema encuentra una manera de satisfacer estas restricciones. Cita del Libro de generación de contenido procesal, capítulo 8 :

Usando los métodos constructivos del Capítulo 3, así como los métodos fractales y de ruido del Capítulo 4, podemos crear varios tipos de datos de salida ajustando los algoritmos hasta que estemos satisfechos con sus datos de salida. Pero si sabemos qué propiedades debe tener el contenido generado, será más conveniente indicar directamente qué queremos que el algoritmo general encuentre contenido que cumpla con nuestros criterios.

Este libro describe la programación de conjuntos de respuestas (ASP), en la que describimos la estructura de lo que trabajamos (los mosaicos son el piso y las paredes, los mosaicos se bordean entre sí), la estructura de las soluciones que estamos buscando (una mazmorra es un grupo fichas conectadas con el principio y el final) y las propiedades de las soluciones (los pasajes laterales deben contener no más de 5 habitaciones, debe haber 1-2 bucles en el laberinto, tres asistentes deben ser derrotados antes de llegar al jefe). Después de eso, el sistema crea posibles soluciones y le permite decidir qué hacer con ellas.

Recientemente, se desarrolló un solucionador de restricciones, que causó gran interés debido a su nombre genial y su curiosa demostración: Wave Function Collapse. [Hay un artículo sobre este solucionador en Habr.] Si le das imágenes de ejemplo para indicar qué restricciones se imponen a los mosaicos vecinos, creará nuevos ejemplos que correspondan a los patrones dados. Su trabajo se describe en WaveFunctionCollapse es Restricción de resolución en la naturaleza :

WFC implementa un método de búsqueda codicioso sin volver atrás. Este artículo explora WFC como un ejemplo de métodos de decisión basados ​​en restricciones.

Ya he logrado mucho con la ayuda de solucionadores de restricciones. Al igual que con el álgebra, antes de aprender a usarlos de manera efectiva, necesito aprender mucho.

Otro ejemplo: la nave espacial que creé . El jugador puede arrastrar motores a cualquier parte, y el sistema determinará qué motores deben activarse al hacer clic en W, A, S, D, Q, E. Por ejemplo, en este barco:


Si desea volar hacia adelante, incluya dos motores traseros. Si desea girar a la izquierda, encienda los motores traseros derecho y delantero izquierdo. Intenté buscar una solución, obligando al sistema a iterar sobre muchos parámetros :


El sistema funcionó, pero no perfecto. Más tarde me di cuenta de que este es otro ejemplo de dónde podría ayudar la solución en la dirección opuesta. Resultó que el movimiento de las naves espaciales puede describirse mediante un sistema lineal de restricciones . Si entendiera esto, podría usar una biblioteca preparada que resuelva con precisión las restricciones, y no mi método de prueba y error, que devuelve una aproximación.

Y otro ejemplo: el proyecto G9.js, en el que puede arrastrar la salida de una determinada función en la pantalla, y determina cómo cambiar los datos de entrada para que coincidan con los datos de salida deseados. ¡Las demos de G9.js se ven geniales! Asegúrese de descomentar la línea "descomentar la siguiente línea" en la demostración de Anillos.

A veces es útil pensar en una tarea en el orden inverso. A menudo resulta que esto me da mejores soluciones que con el razonamiento directo.

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


All Articles