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:
Las celdas con las que estoy trabajando están organizadas en columnas y filas. Tomemos un ejemplo de un juego simple:
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:
Fighter
contendrá un código para manejar golpes de espada, reducir el daño con armadura y un poderoso golpe especial.Mage
contendrá código de manejo de bola de fuego, reflejo de daño y un ataque de congelación especial.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
:
Attack
contendrá un código para manejar columpios, bolas de fuego y ataques con dagas.Defend
contendrá un código para manejar la reducción de daños, reflejar el daño y evitar daños.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.
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.
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:
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:
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.
Veamos un ejemplo: tengo seis clases:
File
,
EncryptedFile
,
GzipFile
,
EncryptedGzipFile
,
BzipFile
,
EncryptedBzipFile
. Puedo descomponerlos en una matriz:
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:
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 :
- Los jugadores deben comenzar el juego lejos de la costa.
- Al subir de nivel, los jugadores deben subir cuesta arriba.
- Los jugadores no deberían poder alcanzar el borde del mapa.
- A medida que crece el nivel, los jugadores deben unirse en grupos.
- Debería haber monstruos simples en las costas sin mucha variación.
- En las llanuras debe haber una gran variedad de monstruos de dificultad media.
- En las zonas montañosas debe haber monstruos jefes complejos.
- 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:
- Los mundos de juego deberían ser islas con muchas costas y un pequeño pico en el centro.
- La altitud debe corresponder a la complejidad de los monstruos.
- En altitudes bajas y altas, debería haber menos variabilidad de biomas que en altitudes medias.
- Los caminos deben permanecer en el mismo nivel de dificultad.
- 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.