https://youtu.be/NtGWCFXvfk8
En vez de 4 en línea puede ser 3, 5, etc.
Lo que tengo no es suficiente y no lo apliqué:
Código
// Signos en línea en tablero de Lado*Lado: // Este programa tiene básicamente 2 etapas, // que se pueden resumir así: // 1: // Desde la situación inicial probar las acciones, // guardando las situaciones nuevas, // y probando acciones en ellas también. // Guardar las situación-acción que causan una victoria, // y las que causan un empate. Es el RE de esa situación, // resultado esperado (porque en otros casos es el que... // se espera o supone si el rival juega bien). // 2: ACTUALIZAR ESTA // En las situaciones no resueltas, probar las acciones. // Si las consecuencias tienen RE, anotar cuantas de ellas... // son victoria de X, 0 o empate y anotar la acción que... // cause la RE más favorable, considerando secundariamente... // las cantidades. // JuntosRequeridos = 2; CasillerosPorFila = 2; // Contando desde 0: MaxColumna = CasillerosPorFila-1; // Los casilleros están numerados así: // 0 1 2 // 3 4 5 // 6 7 8 Casilleros = CasillerosPorFila*CasillerosPorFila; MaxCasillero = Casilleros-1; // Uso arrays porque es más fácil así modificar partes... // de los valores de variables (los casilleros): // Situación a experimentar nro 1, signo en los casilleros // (a este array se le agregarán valores): SaE1 = []; ValoraAgregar = -1; do { ValoraAgregar++; // v significa vacío SaE1.push("v"); // trace("ValoraAgregar es "+ValoraAgregar); } while (ValoraAgregar < MaxCasillero); delete (ValoraAgregar); // Situaciones a experimentar (cuántas son): SaE = 1; NroDeSExperimentandose = 0; NoEvaluedSaE = "SaE1"; SaEEvaluedyJoined = "S"+(eval(NoEvaluedSaE).join("")); // Usada para fácilmente saber de qué signo es turno: set (SaEEvaluedyJoined+"Turno", "X"); // Con LastAPosible, usada para evitar probar... // acciones innecesarias y saber si sólo queda 1: set (SaEEvaluedyJoined+"FirstAPosible", 0); set (SaEEvaluedyJoined+"LastAPosible", MaxCasillero); // Usada a continuación y en varios casos: function PrepararExperimentosEnOtraS () { // trace("Ejecutando PrepararExperimentosEnOtraS"); NroDeSExperimentandose++; trace("Se inician experimentos en otra situación, la anotada nro "+NroDeSExperimentandose); // Usada para evitar concatenar repetidamente: NoEvaluedSaE = "SaE"+NroDeSExperimentandose; SituacionBase = eval(NoEvaluedSaE); SaEEvaluedyJoined = "S"+(SituacionBase.join("")); Turno = eval(SaEEvaluedyJoined+"Turno"); FirstActionPosible = eval(SaEEvaluedyJoined+"FirstAPosible"); LastActionPosible = eval(SaEEvaluedyJoined+"LastAPosible"); Action = FirstActionPosible; } PrepararExperimentosEnOtraS(); // Usada en frame 2 repetidamente: function BuscarBuenasSituacionesEt1 () { // Plantear situación porque puede modificarse pero... // podría requerirse su estado original luego: Situacion = SituacionBase.slice(); // trace("La situación experimentándose es "+Situacion); EvaluarAplicabilidadEt1(); } // Usada luego de que cambia la SaE y/o la acción: function EvaluarAplicabilidadEt1 () { // trace("Ejecutando EvaluarAplicabilidadEt1"); if (Situacion[Action] == "v") { // trace("La acción "+Action+" es realizable."); AplicarAccionEt1(); } else { // trace("La acción "+Action+" no es realizable."); BuscarAccionEt1(); } } // Usada luego de EvaluarAplicabilidadEt1, // si una acción es realizable: function AplicarAccionEt1 () { // trace("Ejecutando AplicarAccionEt1"); Situacion[Action] = Turno; // trace("Se realizó la acción "+Action+"; ahora la situación es "+Situacion); // ¿Ganó? // Si no se detecta victoria vertical, // revisa de otros modos (ver las funciones Chequear): ChequearVertical(); if (Win == "Sí") { // Sí, crear datos sobre eso: // trace("Ganó, ahora se sabe que en la situación "+SituacionBase+" se gana usando la acción "+Action); set (SaEEvaluedyJoined+"BestAction", Action); set (SaEEvaluedyJoined+"RE", Turno); // Se borra la situación de la lista a experimentar, // pues ya se halló la mejor acción en ella: BorrarSaE(); BuscarOtraSituacionEt1(); } else if (FirstActionPosible == LastActionPosible) { // sino si es empate (no hay más acciones posibles): // Parecido, crear datos sobre eso: // trace("Empató, ahora se sabe que en la situación "+SituacionBase+" se empata usando la acción "+Action); // (y es la mejor acción porque no hay otra) set (SaEEvaluedyJoined+"BestAction", Action); set (SaEEvaluedyJoined+"RE", "Empate"); // Se borra la situación de la lista a experimentar, // pues ya se halló la mejor acción en ella: BorrarSaE(); BuscarOtraSituacionEt1(); } else { EvaluarConocimientoSobreSEt1(); // ¿Queda alguna acción sin probar... // en la situación? BuscarAccionEt1(); } } // Usada luego de AplicarAccionEt1, // si causa victoria o empate: function BorrarSaE () { // La última SaE ponerla en la actual: // trace("ATENCIÓN: "+NoEvaluedSaE+", que es "+SaEEvaluedyJoined); set (NoEvaluedSaE, eval("SaE"+SaE).slice()); // trace("...ahora es: "+eval(NoEvaluedSaE)); // Borrar la última: delete (eval("SaE"+SaE)); // Indicar que hay una menos: SaE--; // También se borran datos prácticos que ya no se usarán: // trace("SaEEvaluedyJoined: "+SaEEvaluedyJoined); // trace("SaEEvaluedyJoined+Turno: "+eval(SaEEvaluedyJoined+"Turno")); delete (eval(SaEEvaluedyJoined+"Turno")); // trace("SaEEvaluedyJoined+Turno: "+eval(SaEEvaluedyJoined+"Turno")); delete (eval(SaEEvaluedyJoined+"FirstAPosible")); delete (eval(SaEEvaluedyJoined+"LastAPosible")); // Indicar que vuelva a invertigarse la del número actual, // pues ahora es otra: NroDeSExperimentandose--; } // Usada luego de EvaluarAplicabilidadEt1, // cuando una acción no es realizable: function BuscarAccionEt1 () { // trace("Ejecutando BuscarAccionEt1"); if (Action < LastActionPosible) { // Si queda alguna acción sin probar... // en la situación, probarla: Action++; // Se ejecutará BuscarBuenasSituaciones. } else { // trace("No hay más acciones realizables."); BuscarOtraSituacionEt1(); } } // Usada luego de que hay victoria, empate o si no hay más acciones posibles: function BuscarOtraSituacionEt1 () { // trace("Ejecutando BuscarOtraSituacionEt1"); if (SaE > NroDeSExperimentandose) { PrepararExperimentosEnOtraS(); } else { trace( "No hay más situaciones a experimentar; inicia la etapa 2."); // IniciarEtapa2(); gotoAndStop(4); } } // Usada en AplicarAccionEt1 cuando no hay victoria ni empate: function EvaluarConocimientoSobreSEt1 () { // trace("Ejecutando EvaluarConocimientoSobreSEt1"); SituacionJoined = Situacion.join(""); NombreDeSituacion = "S"+SituacionJoined; ResumenDeBest = NombreDeSituacion+"BestAction"; // ¿Se ha llegado antes a la situación? if (eval(ResumenDeBest) == undefined) { // No, anotar que debe investigarse: SaE++; // Copiar array (porque no se puede normalmente): set ("SaE"+SaE, Situacion.slice()); // trace("Esa situación no está anotada; se anota como nro "+SaE); set(ResumenDeBest, "Desconocida"); if (Turno == "X") { set (NombreDeSituacion+"Turno", "0"); } else { set (NombreDeSituacion+"Turno", "X"); } // Averiguar cual es su 1er acción posible: AccionRevisandose = -1; do { AccionRevisandose++; if (Situacion[AccionRevisandose] == "v") { set (NombreDeSituacion+"FirstAPosible", AccionRevisandose); break; } } while (true); // Averiguar cual es su última acción posible: AccionRevisandose = Casilleros; do { AccionRevisandose--; if (Situacion[AccionRevisandose] == "v") { set (NombreDeSituacion+"LastAPosible", AccionRevisandose); break; } } while (true); } } function ObtenerColumnayFilaDelMarcado () { FilaDelMarcado = Math.floor(Action/CasillerosPorFila); ColumnaDelMarcado = Action%CasillerosPorFila; } function ChequearVertical () { // trace ("Ejecutando ChequearVertical"); // // SOLO FUNCIONA SI CasillerosPorFila >= JuntosRequeridos. // // Se revisará la vertical. Desde el casillero más arriba... // que pueda estar incluído en la línea, // hasta el casillero marcado. Si se haya alguno que no... // tenga la marca requerida, se repite el proceso mirando... // desde uno más abajo del fallado, y mirando ahora hasta... // más abajo, según corresponda: Desde = Action-CasillerosPorFila*(JuntosRequeridos-1); // Mientras el casillero no exista: while (Desde<0) { // trace ("Desde es "+Desde+", se aumentará"); // Indicar que se empiece a mirar desde más abajo: Desde = Desde+CasillerosPorFila; } do { Hasta = Desde+CasillerosPorFila*(JuntosRequeridos-1); if (Hasta>MaxCasillero) { // Para ganar se necesitaría más casilleros hacia... // abajo de los que hay, terminar análisis: // trace ("No hay suficientes cuadraditos abajo"); break; } Puntero = Desde; // trace ("Comenzando chequeo desde "+Desde+" hasta "+Hasta); // Puede cambiar: Win = "Sí"; do { // trace ("Chequando el casillero "+Puntero); if (Situacion[Action] != Situacion[Puntero]) { Win = "No"; Desde = Puntero+CasillerosPorFila; break; } Puntero = Puntero+CasillerosPorFila; } while (Puntero<=Hasta); } while (Desde<=Action && Win == "No"); if (Win == "No") { ChequearHorizontal(); } } function ChequearHorizontal () { // trace ("Ejecutando ChequearHorizontal"); // Es similar al chequeo vertical, pero aquí no sirve... // sumar o restar a un puntero que marque el casillero... // porque puede existir ese casillero pero no estar en... // un costado. En vez de eso, se obtiene su columna y... // fila, se modifica la columna si es posible, // y se calcula cual es el casillero a mirar: ObtenerColumnayFilaDelMarcado(); DesdeColumna = ColumnaDelMarcado-JuntosRequeridos+1; // Para que no mire cuadraditos inexistentes en la izq: if (DesdeColumna<0) { DesdeColumna = 0; } do { HastaColumna = DesdeColumna+JuntosRequeridos-1; if (HastaColumna>MaxColumna) { // Para ganar se necesitaría más casilleros hacia... // la derecha de los que hay, terminar análisis: // trace ("No hay suficientes cuadraditos a la derecha"); break; } Puntero = DesdeColumna; // trace ("Comenzando chequeo desde "+DesdeColumna+" hasta "+HastaColumna); // Puede cambiar: Win = "Sí"; do { CasilleroaMirar = FilaDelMarcado*CasillerosPorFila+Puntero; // trace ("Chequando el casillero "+CasilleroaMirar); if (Situacion[Action] != Situacion[CasilleroaMirar]) { Win = "No"; DesdeColumna = Puntero+1; break; } Puntero++; } while (Puntero<=HastaColumna); } while (DesdeColumna<=ColumnaDelMarcado && Win == "No"); if (Win == "No") { ChequearDesdeUpIzq(); } } // La diagonal así \ function ChequearDesdeUpIzq () { // trace ("Ejecutando ChequearDesdeUpIzq"); DesdeColumna = ColumnaDelMarcado-JuntosRequeridos+1; // trace("DesdeColumna: "+DesdeColumna); DesdeFila = FilaDelMarcado-JuntosRequeridos+1; // trace("DesdeFila: "+DesdeFila); // Para que no mire cuadraditos inexistentes: if (DesdeColumna<DesdeFila) { Sumar = DesdeColumna; } else { Sumar = DesdeFila; } // trace("Sumar: "+Sumar); if (Sumar<0) { // trace("Sumando."); DesdeColumna = DesdeColumna-Sumar; // trace("DesdeColumna: "+DesdeColumna); DesdeFila = DesdeFila-Sumar; // trace("DesdeFila: "+DesdeFila); } do { HastaColumna = DesdeColumna+JuntosRequeridos-1; if (HastaColumna>MaxColumna) { // Para ganar se necesitaría más casilleros hacia... // la derecha de los que hay, terminar análisis: // trace ("No hay suficientes cuadraditos a la derecha"); break; } HastaFila = DesdeFila+JuntosRequeridos-1; // Sirve usar MaxColumna en vez de crear MaxFila... // porque como el tablero es cuadrado serían iguales: if (HastaFila>MaxColumna) { // Para ganar se necesitaría más casilleros hacia... // abajo de los que hay, terminar análisis: // trace ("No hay suficientes cuadraditos abajo"); break; } PunteroDeColumna = DesdeColumna; PunteroDeFila = DesdeFila; // trace ("Comenzando chequeo desde columna "+DesdeColumna+" y fila "+DesdeFila); // trace ("hasta columna "+HastaColumna+" y fila "+HastaFila); // Puede cambiar: Win = "Sí"; do { CasilleroaMirar = PunteroDeFila*CasillerosPorFila+PunteroDeColumna; // trace ("Chequando el casillero "+CasilleroaMirar); if (Situacion[Action] != Situacion[CasilleroaMirar]) { Win = "No"; DesdeColumna = PunteroDeColumna+1; DesdeFila = PunteroDeFila+1; break; } PunteroDeColumna++; PunteroDeFila++; } while (PunteroDeColumna<=HastaColumna); } while (DesdeColumna<=ColumnaDelMarcado && Win == "No"); if (Win == "No") { ChequearDesdeDownIzq(); } } // La diagonal así / function ChequearDesdeDownIzq () { // trace ("Ejecutando ChequearDesdeDownIzq"); DesdeColumna = ColumnaDelMarcado-JuntosRequeridos+1; // trace("DesdeColumna: "+DesdeColumna); DesdeFila = FilaDelMarcado+JuntosRequeridos-1; // trace("DesdeFila: "+DesdeFila); // Para que no mire cuadraditos inexistentes: ColumnasInexistentes = 0; if (DesdeColumna<0) { ColumnasInexistentes = DesdeColumna*-1; } FilasInexistentes = 0; // Está bien usar MaxColumna porque MaxFila sería igual: if (DesdeFila>MaxColumna) { FilasInexistentes = DesdeFila-MaxColumna; } if (ColumnasInexistentes>=FilasInexistentes) { Ajuste = ColumnasInexistentes; } else { Ajuste = FilasInexistentes; } // trace("Ajuste: "+Ajuste); if (Ajuste>0) { // trace("Ajustando."); DesdeColumna = DesdeColumna+Ajuste; // trace("DesdeColumna: "+DesdeColumna); DesdeFila = DesdeFila-Ajuste; // trace("DesdeFila: "+DesdeFila); } do { HastaColumna = DesdeColumna+JuntosRequeridos-1; if (HastaColumna>MaxColumna) { // Para ganar se necesitaría más casilleros hacia... // la derecha de los que hay, terminar análisis: // trace ("No hay suficientes cuadraditos a la derecha"); break; } HastaFila = DesdeFila-JuntosRequeridos+1; if (HastaFila<0) { // Para ganar se necesitaría más casilleros hacia... // arriba de los que hay, terminar análisis: // trace ("No hay suficientes cuadraditos arriba"); break; } PunteroDeColumna = DesdeColumna; PunteroDeFila = DesdeFila; // trace ("Comenzando chequeo desde columna "+DesdeColumna+" y fila "+DesdeFila); // trace ("hasta columna "+HastaColumna+" y fila "+HastaFila); // Puede cambiar: Win = "Sí"; do { CasilleroaMirar = PunteroDeFila*CasillerosPorFila+PunteroDeColumna; // trace ("Chequando el casillero "+CasilleroaMirar); if (Situacion[Action] != Situacion[CasilleroaMirar]) { Win = "No"; DesdeColumna = PunteroDeColumna+1; DesdeFila = PunteroDeFila-1; break; } PunteroDeColumna++; PunteroDeFila--; } while (PunteroDeColumna<=HastaColumna); } while (DesdeColumna<=ColumnaDelMarcado && Win == "No"); // trace("Win: "+Win); }
Resumen:

Mi idea con eso es hallar las mejores acciones en cada posible situación de un tablero de cierto tamaño cuando se requieren cierta cantidad de signos en línea, es decir, si por ejemplo en el "aprendedor" se configura que el tablero tiene 6 casilleros de largo y se requieren 5 signos en línea, el programa aprendería a jugar así, no en 5 casilleros de largo, ni 7, etc, ni con 6 signos requeridos, etc.
Así que en ese sentido no voy por buen camino, ya que no puedo correr el programa poniendo cada pares posibles de valores. Y aunque pudiera programar que los varíe, los resultados son de este estilo:
SvX0vBestAction = 0
es decir el nombre de la variable indica la situación, lo que sucederá es que serán un montón de variables con nombres cada vez más largos, es mejor algo más deductivo, unas reglas generales, siempre y cuando den buenos resultados.
La etapa 2 creo que debería tener en cuenta probabilidades, es decir, que cuando por ejemplo el resultado esperado es un empate, busque llegar a situaciones en que la mayoría de opciones del rival lo lleven a una derrota. Las probabilidades se basarían en la cantidad de opciones y las que salvan al rival, o a uno mismo (si el resultado esperado es derrota, aunque en este juego una IA no debería llegar a eso), no en cuan evidentes para un humano son las opciones mejores, porque eso parece subjetivo y complicado, ni lo intento.
Aún no tengo del todo claro cómo hacer la etapa 2. Y he estado cambiando cosas de la 1. En eso empecé a creer que en la etapa 1 debería guardar las posibilidades o en la 2nda no podría saber las probabilidades, o sería complicado. Creo que debería poner 3 etapas...
1- Se pueban acciones en las posibles situaciones, se van anotando las situaciones, y se anota "la mejor acción" y resultado cuando en la situación sólo hay 1 acción posible.
2- En las situaciones que quedaron sin resolver, se prueban acciones...
No sé bien... en ese punto una acción puede:
- Llevar a una situación resuelta, que luego será un empate o derrota.
- Llevar a una situación no resuelta.
- Llevar a una victoria.
Supongamos que está en:
0XX
XX0
0AB
Turno del 0.
Si pone en A, lleva a una situación resuelta, sólo queda una posible jugaba, B, y es empate.
Si pone en B, también es resuelta, pero es derrota.
Entonces deben crearse unas variables digamos así:
S0XXXX00vvWinX = 1
S0XXXX00vvEmpate = 1
marcando que en esa situación hay 1 posibilidad de que gane X y otra de que haya empate. La idea es que en la etapa 3, cuando haya que elegir cual es la mejor acción en una situación anterior a esta, cuando se llegue a esta se vea que X tiene 1/2 de probabilidad de ganar, aunque el resultado esperado sea empate, logrando entonces que el programa deduzca que para X es una situación más conveniente a llegar que otras, por ejemplo:
0CX
XX0
0AB
Turno de X. Si 0 juega bien, será empate, pero X poniendo en C o A tiene probabilidades de ganar, poniendo en B no.
Se trata de guardar posibilidades y luego guardar las acciones que lleven a situaciones en que haya más probabilidades de ganar.
Otra posibilidad es que lleve a una victoria, ejemplo:
0Xv
0Xv
vvv
Turno del X.
Lo que le conviene a X es evidente, pero... 0 debe tener en cuenta la posibilidad de que no haga eso. En tableros más grandes, puede que se deba elegir entre 2 situaciones inconvenientes, quizá en una X tiene 2 formas de ganar y en la otra sólo 1, por lo tanto debe grabarse la 2nda. Para eso debe haber datos sobre qué puede suceder, y por eso aunque la mejor acción aquí sea evidente, la situación no se puede resolver mientras no se tengan datos de las próximas, para saber exactamente qué tan probable es que gane X en esta (parece 1/5 pero es más complejo), para que previamente 0 pueda saber si esta situación es peor que otra alternativa, como expliqué.
Todo esto es como muy rebuscado, creo que sólo tiene sentido en tableros grandes. Normalmente la IA no llegaría a situaciones en que pueda perder. A ver un caso más realista:
vvvv
vXvv
vvvv
vvvv
Turno de 0, se requieren 3 para ganar. Parece que no importa lo que haga, perderá, pero la idea es que tenga las mayores probabilidades posibles de ganar o al menos empatar.
En fin, parece que entonces es cuestión de ir hacia atrás, en orden, es decir, resolver 1ero las situaciones en que sólo 1 acción es posible, luego las que sólo tienen 2, etc. Aunque en algunas sea obvio cómo ganar, no se pueden considerar resueltas antes de tiempo. Así que sería:
Etapa 1: Las acciones pueden llevar a situaciones nuevas, no resueltas, empates o victorias.
Etapa 2: Las acciones pueden llevar a situaciones no resueltas, resueltas o victorias.
Etapa 3: Las acciones pueden llevar a situaciones no resueltas o resueltas.
Considero importantes las diferencias para evitar ifs en vano...
Una posible mejora del programa sería, en vez de partir de un tablero vacío e ir grabando situaciones a las que se llegue, generar las posibles situaciones finales, resolverlas, generar las situaciones previas, resolverlas, y así. O en vez de tener 1 lista de situaciones, tener una por cada cantidad de signos puestos, o sea:
Una lista de situaciones en que sólo 1 acción es posible.
Otra en que sólo 2 acciones son posibles.
Etc.
Son mejoras alternativas. Sirven para evitar que el programa analice repetidamente situaciones que sabemos que aún no podrá resolver.
¿Ustedes cómo lo harían? Un programa que aprenda a jugar expertamente Signos en línea.