Implementar MENACE

11

Antecedentes

AMENAZA ( M áquina E Ducable N oughts Un nd C rosses E Ngine) es un algoritmo de aprendizaje automático superficial rudimentario para los Ceros de juego y cruces, creadas por el informático británico Donald Michie en la década de 1960. Originalmente se implementó con 304 cajas de fósforos, cada una etiquetada con una posición de tablero y que contenía cuentas de colores (con uno de nueve colores, que representan posibles movimientos). Michie calculó que estas 304 cajas de fósforos eran suficientes para cada combinación de movimientos en el tablero.

Los más matemáticos entre ustedes pueden darse cuenta de que en realidad hay 19,683 combinaciones posibles de Noughts, Crosses y Blanks en un tablero de N&C; sin embargo, calculó formas de reducir este número (para acelerar el algoritmo y probablemente reducir las cajas de fósforos). En primer lugar, eliminó todos los movimientos no posibles, como:

-------
|X|0|X|
| |0| |
|X|X| |
-------

(dos ceros y cuatro cruces)

Luego, compensó las rotaciones. Por ejemplo, si en la caja de fósforos vemos:

-------
| |0|0|
|X| |X|
| |0| |
-------

podemos usar la misma caja para

-------
| |X| |
|0| |0|
| |X|0|
-------

Por lo tanto, las cuentas de colores antes mencionadas representan posiciones relativas, no absolutas. Por ejemplo, si dijéramos que una cuenta roja significaba en la parte superior izquierda, entonces miraríamos la imagen en la parte superior de la caja y veríamos:

-------
| |0|0|
|X| |X|
| |0| |
-------

entonces sabríamos que en el caso de que este sea el tablero, la cuenta roja significaría:

-------
|R|0|0|
|X| |X|
| |0| |
-------

Pero si este es el tablero:

-------
| |X| |
|0| |0|
| |X|0|
-------

la cuenta roja significaría

-------
| |X|R|
|0| |0|
| |X|0|
-------

Estas transformaciones se aplicaron para rotaciones e inversiones (en todas las direcciones, incluida la diagonal). Una vez más, solo necesita guardar cada caja de fósforos una vez de esta manera: ¡no haga cajas virtuales separadas para cada transformación!

Otra simplificación que hizo Michie fue asegurarse de que la computadora funciona primero. De esta forma, podría eliminar todos los movimientos de primer nivel, eliminando aproximadamente una quinta parte de las cajas restantes. Finalmente, eliminó todas las casillas de finalización del juego (ya que no se requerían más "contenidos" o movimientos en estos pasos).

Bien, ahora en el algoritmo mismo (es muy simple):

  1. Primero, decida qué representan los colores de las cuentas. Necesitarás 9 colores para representar cada uno de los espacios en el tablero.
  2. Al comienzo del juego, cada una de las 304 cajas de fósforos contiene cuentas. Si bien las cuentas son de color aleatorio (por lo que los duplicados están bien), deberían ser posibles movimientos (por lo tanto, si la imagen del estado del tablero representa una 'O' en el centro-derecha, entonces no puede usar la cuenta que representa el centro) Correcto).
  3. Cada vez que sea el turno de MENACE (X), encuentre la caja de fósforos con la posición actual del tablero (o alguna transformación) impresa en él.
  4. Abra la caja de fósforos y elija cualquier cuenta allí al azar.
  5. Descubra cómo se ha transformado el estado del tablero para llegar a la imagen en la caja de fósforos (por ejemplo, girado 90 grados en sentido antihorario). Luego, aplique esa transformación a la cuenta (por ejemplo, la parte superior izquierda se convierte en izquierda-izquierda).
  6. Coloca una X en ese cuadrado. Retire la cuenta seleccionada de la caja de fósforos. Si el cuadro se deja vacío como resultado, coloque tres cuentas al azar (posibles) en el cuadro y elija una de ellas para el movimiento.
  7. Repita 3-6 hasta que termine el juego.
  8. Si MENACE ganó el juego, revisa cada caja de cerillas que tomó MENACE. Luego, rastrea el color de la cuenta que usó en ese movimiento. Ponga dos de ese color de cuenta en la caja (para que haya la cuenta original + una más, aumentando así la probabilidad de que MENACE haga ese movimiento la próxima vez que llegue a esa posición)
  9. Si MENACE perdió el juego, no haga nada ( no reemplace las cuentas que sacó).
  10. Si MENACE dibujó el juego, reemplace la cuenta que usó en cada uno de sus movimientos, pero no agregue uno adicional, para que le quede lo que comenzó.

Esto nos deja con un algoritmo que es muy simple, pero difícil de implementar. Esto forma la base de su desafío.

Si todavía está confundido, consulte http://chalkdustmagazine.com/features/menace-machine-educable-noughts-crosses-engine/ : es lo que leí la primera vez que aprendí sobre este algoritmo

Desafío

Juega un juego de Tic-Tac-Toe con la computadora. En cada paso, muestre el contenido de todas las cajas de fósforos.

Entradas

  • Al comienzo del programa, un número que indica cuántos juegos quieres jugar contra MENACE
  • Luego, después del primer turno de MENACE, ingresa su movimiento como una cadena de dos caracteres, la primera letra es "L", "R" o "M" (izquierda, derecha o centro) haciendo referencia al eje Y. Luego ingresa otra letra (nuevamente, "L", "R" o "M"), esta vez refiriéndose al eje X. Repita para todos los movimientos y juegos.

Salidas

  • Al comienzo de cada nuevo juego, salida "nuevo juego".
  • Después de cada movimiento del jugador, saca el tablero en cualquier formato razonable. No necesita verse bonita (por ejemplo, una matriz de matrices que representan las posiciones del tablero está bien).
  • Después de cada movimiento del jugador, MENACE debe hacer un movimiento. Salida del tablero después del movimiento de MENACE
  • Después de cada juego, muestra el contenido de todas las 304 cajas de fósforos. Las cuentas se pueden representar con una letra, nombre de un color, carácter o cualquier cadena o número entero que desee (sin punteros, funciones anónimas, etc.).

Reglas

  1. Este es el , por lo que la respuesta más corta en bytes gana.
  2. Debo poder ingresar movimientos después de ver la respuesta de MENACE. No 'pase todos sus movimientos a esta función y observe cómo se desarrolla el juego'.
  3. El tablero debe ser despejado entre juegos.
  4. Las cajas de fósforos no deben borrarse entre juegos (esto restablecería el aprendizaje automático)
  5. Debes tener 304 cajas de fósforos. Cualquiera puede implementar este algoritmo con todas las 19.683 cajas de fósforos, pero el aprendizaje es lento (ya que requiere muchos juegos para obtener todos ellos con contenidos útiles).
  6. La salida puede estar en cualquier formato razonable, y la entrada se puede tomar según los estándares PPCG (siempre que cumpla con la regla 2). Si necesita ajustar el formato de entrada (como se describe en la sección ' Entrada '), entonces está bien siempre que tenga sentido.
  7. Un juego termina cuando un jugador gana (al obtener tres en fila en diagonal, horizontal o vertical) o si hay un empate (el tablero está lleno y no hay ganador)
  8. Si bien MENACE necesita hacer movimientos posibles (y solo tener cuentas posibles dentro de cada caja de fósforos), por el desafío no es necesario validar la entrada del usuario. Si escriben algo incorrecto, su programa puede hacer lo que sea (volverse completamente loco, arrojar un error, etc.): puede suponer que la entrada es correcta.
Geza Kerecsenyi
fuente
Recuerdo que Martin Gardner demostró la idea usando el juego más simple Hexapawn, aunque olvido lo que llamó la "computadora" que construyó.
Neil
1
Gran reto. Un par de preguntas rápidas: 1. Una vez que hay más de una cuenta en un espacio dado en un cuadro, ¿cómo se debe representar eso en la salida? 2. ¿Realmente desea que salgan todas las 304 cajas (2736 celdas) después de cada movimiento?
Nick Kennedy el
@ NickKennedy Gracias por los comentarios. La manera en que yo esperaría que las perlas se pueden representar cuando se registra es como una matriz (aunque puede hacerlo de forma diferente a no restringir diferentes idiomas), por ejemplo, si ha elegido para representar números de las cuentas: [[0, 2, 6], [4, 8, 4, 3, 3], [7, 7, 7, 7, 7, 7, 7, 8], [1], ... [3, 3, 5, 4]].
Geza Kerecsenyi

Respuestas:

3

R , 839 bytes

options(max.print=1e5)
s=colSums
r=rowSums
m=matrix
a=array
y=apply
S=sum
p=sample
b=m(rep(i<-1:(K=3^9),e=9)%/%(E=3^(8:0))%%3,c(9,K))
V=a(1:9,c(3,3,8))
V[,,2:4]=c(V[x<-3:1,,1],V[,x,1],V[x,x,1])
V[,,5:8]=y(V[,,1:4],3,t)
d=aperm(a(b[c(V),],c(9,8,K)),c(1,3,2))
v=m(V,9)
g=y(m(match(e<-y(d*E,2:3,S),i),,8),1,min)
g[K]=K
G=9-y(t(e==g)*8:1,2,max)
h=s(a(c(b,d[,,5],b[c(1,5,9,3,5,7,1:3),]),c(3,3,K,3))*3^(0:2))
k=r(s(h==13))>0
l=r(s(h==26))>0
o=s(b>0)
M=b
M[M==0]=-1
repeat{A=b[,t<-K]
z=j=c();B=1
repeat{if(S(pmax(-M[,t],0))<1)M[p(9,pmin(3,S(x)),,x<-M[,t]<1),t]=-1
z=c(z,u<-p(9,1,,pmax(-M[,t],0)))
j=c(j,t)
A[v[,G[B]][u]]=1
print(m(A,3))
if(k[B<-S(A*E)]||o[B]==9)break
A[ceiling((utf8ToInt(readline())-76)/5)%*%c(1,3)+1]=2
if(l[B<-S(A*E)])break
t=g[B]}
M[x]=M[x<-cbind(z,j)]-k[B]+l[B]
print(a(M[,g==seq(g)&!k&!l&s(b==1)==s(b==2)&o<8],c(3,3,304)))}

Pruébalo en línea!

Una respuesta bastante larga, pero este no fue un desafío directo. El enlace TIO aquí fallará porque espera una entrada interactiva. Aquí hay una versión que juega contra un segundo jugador aleatorio que simplemente elige un lugar al azar. El resultado de esta segunda versión es solo el ganador (1, 2 o 0 para un sorteo). Las cajas de fósforos se inicializan para todas las posiciones del tablero, pero solo se usan para el 304 según la especificación. Se implementan como copias del tablero con números negativos para indicar el número de cuentas en cada posición. Experimenté con una lista de vectores según la especificación original, pero fue menos intuitiva.

Esta es una versión menos golfizada con comentarios (pero aún nombres de variables cortos). Tenga en cuenta que no imprime las cajas de fósforos porque son muy largas. Puede implementar un jugador interactivo 2, un jugador aleatorio 2 o la misma estrategia de caja de fósforos para el jugador 2.

auto = 1 # 1 = Random player 2, 2 = Player 2 uses learning strategy
         # 0 for interactive
print_board <- function(board) {
  cat(apply(matrix(c(".", "X", "O")[board + 1], 3), 1, paste, collapse = ""), "", sep = "\n")
}
E = 3 ^ (8:0) # Number of possible arrangements of board
              # ignoring rotations etc.
# Make all possible boards
b = matrix(rep(1:3 ^ 9, e = 9) %/% E %% 3, c(9, 3 ^ 9))
# Define the eight possible rotation/inversion matrices
V = array(1:9, c(3, 3, 8))
V[, , 2:4] = c(V[x <- 3:1, , 1], V[, x, 1], V[x, x, 1])
V[, , 5:8] = apply(V[, , 1:4], 3, t)
# Create eight copies of the 19683 boards with each transformation
d = aperm(array(b[c(V), ], c(9, 8, 3 ^ 9)), c(1, 3, 2))
v = matrix(V, 9)
# Create reverse transformations (which are the same except for rotation)
w = v[, c(1:5, 7, 6, 8)]
# Find the sums of each transformation using base 3
e = apply(d * E, 2:3, sum)
# Find the lowest possible sum for each board's transformed versions
# This will be the one used for the matchboxes
f = matrix(match(e, 1:3 ^ 9), , 8)
g = apply(f, 1, min)
# Store which transformation was necessary to convert the lowest board
# into this one
G = 9 - apply(t(e == g) * 8:1, 2, max)
# Work out which boards have 3-in-a-row
h = colSums(array(c(b, d[, , 5], b[c(1, 5, 9, 3, 5, 7, 1:3), ]), c(3, 3, 3 ^ 9, 3)) * 3 ^ (0:2))
k = rowSums(colSums(h == 13)) > 0 # player 1 wins
l = rowSums(colSums(h == 26)) > 0 # player 2 wins
# Store how many cells are filled
o = colSums(b > 0)
# Create matchboxes. These contain the actual board configuration, but
# instead of zeroes for blanks have a minus number. This is initially -1,
# but will ultimately represent the number of beads for that spot on the
# board.
M = b
M[M == 0] = -1
repeat {
  # Initialise board and storage of moves and intermediate board positions
  A = b[, t <- 3 ^ 9]
  z = j = c()
  C = 1
  # If we're automating player 2 also, initialise its storage
  if (auto) {
    Z = J = c()
  }
  repeat {
    # If the current board's matchbox is empty, put up to three more beads
    # back in
    if (sum(pmax(-M[, t], 0)) == 0) {
      M[sample(9, pmin(3, sum(x)), , x <- M[, t] == 0), t] = -1
    }
    # Take out a bead from the matchbox
    u = sample(9, 1, , pmax(-M[, t], 0))
    # Mark the bead as taken out
    M[u, t] = M[u, t] + 1
    # Store the bead and board position in the chain for this game
    z = c(z, u)
    j = c(j, t)
    # Mark the spot on the board
    A[v[, C][u]] = 1
    # Print the board
    if (!auto) print_board(matrix(A, 3))
    # Check if  player 1 has won or board is full
    if (k[B <- sum(A * E)] || o[B] == 9) break
    if (auto) {
      # Repeat for player 2 if we're automating its moves
      # Note if auto == 1 then we pick at random
      # If auto == 2 we use the same algorithm as player 1
      D = g[B]
      if (sum(pmax(-M[, D], 0)) == 0) {
        M[sample(9, pmin(3, sum(x)), , x <- M[, D] == 0), D] = -1
      }
      U = sample(9, 1, , if (auto == 1) M[, D] <= 0 else pmax(-M[, D], 0))
      Z = c(Z, U)
      J = c(J, D)
      A[v[, G[B]][U]] = 2
    } else {
      cat(
        "Please enter move (LMR for top/middle/bottom row and\nLMR for left/middle/right column, e.g. MR:"
      )
      repeat {
        # Convert LMR into numbers
        q = ceiling((utf8ToInt(readline()) - 76) / 5)
        if (length(q) != 2)
          stop("Finished")
        if (all(q %in% 0:2) && A[q %*% c(1, 3) + 1] == 0) {
          break
        } else {
          message("Invalid input, please try again")
        }
      }
      A[q %*% c(1, 3) + 1] = 2
    }
    if (l[B <- sum(A * E)])
      break
    # Player 2 has won
    t = g[B]
    C = G[B]
  }
  if (auto) {
    cat(c("D", 1:2)[1 + k[B] + 2 * l[B]])
  } else {
    cat("Outcome:", c("Draw", sprintf("Player %d wins", 1:2))[1 + k[B] + 2 * l[B]], "\n")
  }
  # Add beads back to matchbox
  M[x] = M[x <- cbind(z, j)] - k[B] - 1 + l[B]
  if (auto)
    M[x] = M[x <- cbind(Z, J)] - l[B] - 1 + k[B]
}
Nick Kennedy
fuente
¡Muy inteligente! Por supuesto, las rotaciones lo hacen difícil, ¡pero gracias por agregar también el reproductor de bot!
Geza Kerecsenyi