Parcialmente observable Connect-4

8

El juego

Jugarás un juego (casi) estándar de Connect-4 . Desafortunadamente, es un juego por correspondencia y alguien ha colocado cinta negra en cada segunda fila comenzando desde abajo, de modo que no puede ver ninguno de los movimientos de su oponente dentro de estas filas.

Cualquier movimiento dentro de columnas ya completas contará como pasar tu turno, y si un juego dura más de un 6 * 7turno, se adjudicará como un empate.

Especificación de desafío

Su programa debe implementarse como una función de Python 3. El primer argumento es una 'vista' del tablero, que representa el estado conocido del tablero como una lista 2D de filas de abajo hacia arriba donde 1hay un movimiento del primer jugador, 2un movimiento del segundo jugador y 0una posición vacía u oculta moverse por tu oponente.

El segundo argumento es un número de turno indexado 0, y su paridad te dice de qué jugador eres.

El argumento final es un estado arbitrario, inicializado al Nonecomienzo de cada juego, que puedes usar para preservar el estado entre turnos.

Debes devolver una tupla de 2 tuplas del índice de columna que deseas jugar y el nuevo estado que se te devolverá el próximo turno.

Puntuación

Una victoria cuenta como +1, un empate como 0y una pérdida como -1. Su objetivo es lograr el puntaje promedio más alto en un torneo round-robin. Trataré de ejecutar tantos partidos como sea necesario para identificar un ganador claro.

Reglas

Cualquier competidor debe tener como máximo un robot competidor en cualquier momento, pero está bien actualizar su entrada si realiza mejoras. Intenta limitar tu bot a ~ 1 segundo de tiempo de reflexión por turno.

Pruebas

Aquí está el código fuente del controlador, junto con algunos ejemplos de bots no competitivos para referencia:

import itertools
import random

def get_strides(board, i, j):
    yield ((i, k) for k in range(j + 1, 7))
    yield ((i, k) for k in range(j - 1, -1, -1))
    yield ((k, j) for k in range(i + 1, 6))
    yield ((k, j) for k in range(i - 1, -1, -1))
    directions = [(1, 1), (-1, -1), (1, -1), (-1, 1)]
    def diag(di, dj):
        i1 = i
        j1 = j
        while True:
            i1 += di
            if i1 < 0 or i1 >= 6:
                break
            j1 += dj
            if j1 < 0 or j1 >= 7:
                break
            yield (i1, j1)
    for d in directions:
        yield diag(*d)

DRAWN = 0
LOST = 1
WON = 2
UNDECIDED = 3

def get_outcome(board, i, j):
    if all(board[-1]):
        return DRAWN
    player = board[i][j]
    strides = get_strides(board, i, j)
    for _ in range(4):
        s0 = next(strides)
        s1 = next(strides)
        n = 1
        for s in (s0, s1):
            for i1, j1 in s:
                if board[i1][j1] == player:
                    n += 1
                    if n >= 4:
                        return WON
                else:
                    break
    return UNDECIDED

def apply_move(board, player, move):
    for i, row in enumerate(board):
        if board[i][move] == 0:
            board[i][move] = player
            outcome = get_outcome(board, i, move)
            return outcome
    if all(board[-1]):
        return DRAWN
    return UNDECIDED

def get_view(board, player):
    view = [list(row) for row in board]
    for i, row in enumerate(view):
        if i % 2:
            continue
        for j, x in enumerate(row):
            if x == 3 - player:
                row[j] = 0
    return view

def run_game(player1, player2):
    players = {1 : player1, 2 : player2}
    board = [[0] * 7 for _ in range(6)]
    states = {1 : None, 2 : None}
    for turn in range(6 * 7):
        p = (turn % 2) + 1
        player = players[p]
        view = get_view(board, p)
        move, state = player(view, turn, states[p])
        outcome = apply_move(board, p, move)
        if outcome == DRAWN:
            return DRAWN
        elif outcome == WON:
            return p
        else:
            states[p] = state
    return DRAWN

def get_score(counts):
    return (counts[WON] - counts[LOST]) / float(sum(counts))

def run_tournament(players, rounds=10000):
    counts = [[0] * 3 for _ in players]
    for r in range(rounds):
        for i, player1 in enumerate(players):
            for j, player2 in enumerate(players):
                if i == j:
                    continue
                outcome = run_game(player1, player2)
                if outcome == DRAWN:
                    for k in i, j:
                        counts[k][DRAWN] += 1
                else:
                    if outcome == 1:
                        w, l = i, j
                    else:
                        w, l = j, i
                    counts[w][WON] += 1
                    counts[l][LOST] += 1
        ranks = sorted(range(len(players)), key = lambda i: get_score(counts[i]), reverse=True)
        print("Round %d of %d\n" % (r + 1, rounds))
        rows = [("Name", "Draws", "Losses", "Wins", "Score")]
        for i in ranks:
            name = players[i].__name__
            score = get_score(counts[i])
            rows.append([name + ":"] + [str(n) for n in counts[i]] + ["%6.3f" % score])
        lengths = [max(len(s) for s in col) + 1 for col in zip(*rows)]
        for i, row in enumerate(rows):
            padding = ((n - len(s)) * ' ' for s, n in zip(row, lengths))
            print(''.join(s + p for s, p in zip(row, padding)))
            if i == 0:
                print()
        print()

def random_player(view, turn, state):
    return random.randrange(0, 7), state

def constant_player(view, turn, state):
    return 0, state

def better_random_player(view, turn, state):
    while True:
        j = random.randrange(0, 7)
        if view[-1][j] == 0:
            return j, state

def better_constant_player(view, turn, state):
    for j in range(7):
        if view[-1][j] == 0:
            return j, state

players = [random_player, constant_player, better_random_player, better_constant_player]

run_tournament(players)

Happy KoTHing!

Resultados provisionales

Name                    Draws Losses Wins  Score  

zsani_bot:              40    5377   94583  0.892 
better_constant_player: 0     28665  71335  0.427 
constant_player:        3     53961  46036 -0.079 
normalBot:              38    64903  35059 -0.298 
better_random_player:   192   71447  28361 -0.431 
random_player:          199   75411  24390 -0.510 
user1502040
fuente
¿Podría explicar por qué verifica la vista [-1] [j] == 0? No estoy completamente seguro de ver dónde los llenaste y mi conocimiento de Python parece estar un poco oxidado.
Barbarian772
@ Barbarian772 Estoy comprobando si todavía hay espacio en esa columna. Tenga en cuenta que hay 6 filas, por lo que la fila superior se observa completamente.
user1502040
1
No debe contar la colocación en columnas ya completas como un pase. Muchos juegos de conectar 4 terminan con solo una columna no llena y si un jugador pierde al jugar en esa columna, esto hará que el juego empate cuando de lo contrario es una victoria forzada para un jugador.
soktinpk
@soktinpk ¿Eso no solo agrega otra capa de estrategia? Después de todo, Connect-4 es un juego resuelto, por lo que el factor de salto de turno podría ser un cambio de regla suficiente para que los contribuyentes no puedan usar los algoritmos estándar.
mypetlion
1
¿Indización cero desde abajo, son las filas pegadas (0,2,4,6) o (1,3,5)? Algún arte ASCII sería útil.
SIGSTACKFAULT

Respuestas:

6

Este bot se lleva todas las victorias seguras y retrocede para bloquear a los rivales, luego adivinarlos vertical y horizontalmente o hacer movimientos aleatorios.

importar pprint, matemáticas, colecciones, copiar
def zsani_bot_2 (ver, girar, estado):
    if state == None: #primer turno propio - siempre para el medio
        state = (1, 2) if turn == 0 else (2, 1) # (my_symbol, your symbol)
        #print (pprint.pformat (view) + 'Turn:' + str (turn) + 'Player:' + str (state [0]))
        retorno 3, estado

    # ubicar puntos obvios
    para i en rango (1, 6): #skip primera fila
        para j en rango (len (ver [i])): #TODO: Optimizar con zip. Ve por la claridad ahora
            if view [i] [j]! = 0 y view [i-1] [j] == 0:
                vista [i-1] [j] = estado [1]
    puntos_enemigos = piso de matemáticas (turno / 2)
    ++ puntos_enemigos si el estado [0] == 2 más puntos_enemigos
    puntos_conocidos = suma ([i.count (estado [1]) para i a la vista])
    missing_points = puntos_enemigos - puntos_conocidos

    # Asegúrate de ganar en cualquier dirección
    para j en rango (0, 7): # cada columna
        para i en rango (4, -1, -1):
            si vista [i] [j]! = 0:
                romper # encontrar el punto relleno más alto conocido
        if (no faltan_puntos o i + 1 en {1, 3, 5}):
            view1 = copy.deepcopy (ver)
            intento = apply_move (view1, estado [0], j)
            si intento == GANADO:
               # print (pprint.pformat (ver) + 'Turno:' + str (turno) + 'Jugador:' + str (estado [0]) + 'movimiento ganador')
                retorno j, estado

    #bloquea que el enemigo gane en cualquier dirección
    para j en el rango (0, 7):
        para i en rango (4, -1, -1):
            si vista [i] [j]! = 0:
                romper # encontrar el punto relleno más alto conocido
        if (no faltan_puntos o (i + 1 en {1, 3, 5})):
            view1 = copy.deepcopy (ver)
            intento = apply_move (view1, estado [1], j)
            si intento == GANADO:
              # print (pprint.pformat (ver) + 'Turno:' + str (turno) + 'Jugador:' + str (estado [0]) + 'movimiento de guardado')
                retorno j, estado

    #muros de bloque
    para i en el rango (0, 3): # imposible obtener 4 en una fila cuando la columna está llena
        para j en el rango (0, 6):
            if view [i] [j]! = 0 y view [i] [j] == view [i + 1] [j] y view [i + 2] [j] == view [i + 3] [j ] == 0:
             # print (pprint.pformat (view) + 'Turn:' + str (turn) + 'Player:' + str (state [0]) + 'column move')
                retorno j, estado

    #bloquear plataformas si posee información perfecta en la fila de abajo y el punto de caída
    para i en rango (0, 5):
        para j en el rango (0, 3):
            stats = collections.Counter ([ver [i] [j], ver [i] [j + 1], ver [i] [j + 2], ver [i] [j + 3]])
            if stats [0] == 2 and (stats [state [0]] == 2 o stats [state [0]] == 2):
                para k en el rango (0, 3):
                    si ver [i] [j + k] == 0:
                        rotura
                if (i == 0 o view [i-1] [j + k]! = 0) y (no faltan_puntos o i en {1, 3, 5}):
                    #print (pprint.pformat (ver) + 'Turno:' + str (turno) + 'Jugador:' + str (estado [0]) + 'movimiento de plataforma')
                    devuelve j + k, estado
                más:
                    para l en rango (k, 3):
                        si ver [i] [j + l] == 0:
                            rotura
                        if (i == 0 o view [i-1] [j + l]! = 0) y (no faltan_puntos o i en {1, 3, 5}):
                     # print (pprint.pformat (ver) + 'Turno:' + str (turno) + 'Jugador:' + str (estado [0]) + 'movimiento de plataforma')
                            devuelve j + l, estado

    #backback -> aleatorio
    mientras cierto:
        j = random.randrange (0, 7)
        si la vista [-1] [j] == 0:
            #print (pprint.pformat (ver) + 'Turno:' + str (turno) + 'Jugador:' + str (estado [0]) + 'movimiento aleatorio')
            retorno j, estado

Gracias por arreglar run_game!

Registro de cambios:

  • v2 agrega bloqueo horizontal: si, en una fila de 4, hay dos espacios vacíos y dos espacios ocupados por el mismo jugador, intentará llenar uno de ellos para tener tres en una fila / bloquear la fila de los oponentes, lo que con suerte se capitalizará en los siguientes turnos.
Syfer Polski
fuente
3
Bienvenido al sitio. Voté para rechazar la edición para cambiar al código, sería mejor dejarlo como comentario, de esa manera el OP puede decidir qué hacer con el código.
Ad Hoc Garf Hunter
No tengo suficiente reputación para comentar en la publicación principal. ¿Cómo retiro una edición?
Syfer Polski,
No es necesario retirar la edición (no creo que pueda de todos modos). En el futuro, los comentarios serán suficientes, pero como lo ha dicho en su respuesta, es probable que el OP lo vea. Además, creo que el OP verá que sugirió y editó incluso si ha sido rechazado.
Ad Hoc Garf Hunter
La razón por la que me gustaría retirar la edición es porque omití una línea en los cambios, sin la cual el código editado no se ejecutará por completo. Lo he incluido en la sugerencia de edición en mi publicación. ¡Gracias por tu ayuda!
Syfer Polski
2

normalBot juega bajo el supuesto de que los puntos en el medio son más valiosos que los puntos en los extremos. Por lo tanto, utiliza una distribución normal centrada en el medio para determinar sus elecciones.

def normalBot(view, turn, state):
    randomNumber = round(np.random.normal(3, 1.25))
    fullColumns = []
    for i in range(7):
        if view[-1][i] != 0:
            fullColumns.append(i)
    while (randomNumber > 6) or (randomNumber < 0) or (randomNumber in fullColumns):
        randomNumber = round(np.random.normal(3, 1.25))
    return randomNumber, state
Bob Cratchit
fuente
-1

Obviamente, esto no es competitivo, pero de todos modos es muy útil para la depuración ... y sorprendentemente divertido, si conoces a tu bot lo suficientemente bien como para engañar: D, así que publico con la esperanza de inspirar más presentaciones:

import math, pprint
def manual_bot(view, turn, state):
    if state == None:
        state = (1, 2) if turn == 0 else (2, 1) #(my_symbol, your symbol)

#locate obvious points
    for row in range (1, 6):
        for j in range(len(view[row])):
            if view[row][j] != 0 and view[row-1][j] == 0:
                view[row-1][j] = state[1]

#if you're second, the opponent has one more point than half the turns
    enemy_points = math.ceil(turn/2)
    known_points = sum([row.count(state[1]) for row in view])
    missing_points = enemy_points - known_points

    print(pprint.pformat(view) + ' Turn: ' + str(turn) + ' Player: ' + str(state[0]) + ' Missing points: ' + str(missing_points))
    while True:
        try:
            move = int(input("What is your move?(0-6) "))
        except ValueError:
            continue
        if move in {0, 1, 2, 3, 4, 5, 6}:
            return move, state

La cuadrícula está al revés (la fila inferior es la más alta). Para obtener los anuncios de los ganadores, debe parchear el controlador del juego, agregando una declaración de impresión antes de devolver la victoria:

elif outcome == WON:
    print(pprint.pformat(board) + ' Turn: ' + str(turn) +' Winner: '+ str(p))
    return p

[[0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0]] Turno: 0 Jugador: 1 Puntos faltantes: 0
¿Cuál es tu jugada? (0-6) 3
[[0, 0, 0, 1, 0, 0, 0],
 [0, 0, 0, 2, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0]] Turno: 2 Jugador: 1 Puntos faltantes: 0
¿Cuál es tu movimiento? (0-6) 2
[[0, 0, 1, 1, 0, 0, 0],
 [0, 0, 0, 2, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0]] Turno: 4 Jugador: 1 Puntos faltantes: 1
¿Cuál es tu movimiento? (0-6) 4
[[0, 0, 1, 1, 1, 0, 0],
 [0, 0, 0, 2, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0]] Turno: 6 Jugador: 1 Puntos faltantes: 2
¿Cuál es tu movimiento? (0-6) 1
[[2, 1, 1, 1, 1, 2, 0],
 [0, 0, 0, 2, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0]] Turno: 6 Ganador: 1
[[0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0]] Turno: 1 Jugador: 2 Puntos faltantes: 1
¿Cuál es tu movimiento? (0-6) 2
[[0, 0, 2, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0]] Turno: 3 Jugador: 2 Puntos faltantes: 2
¿Cuál es tu jugada? (0-6) 3
[[0, 0, 2, 1, 0, 0, 0],
 [0, 0, 1, 2, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0]] Turno: 5 Jugador: 2 Puntos faltantes: 1
¿Cuál es tu movimiento? (0-6) 4
[[0, 0, 2, 1, 2, 0, 0],
 [0, 0, 1, 2, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0]] Turno: 7 Jugador: 2 Puntos faltantes: 2
¿Cuál es tu movimiento? (0-6) 1
[[0, 2, 2, 1, 2, 0, 0],
 [0, 0, 1, 2, 0, 0, 0],
 [0, 0, 1, 0, 0, 0, 0],
 [0, 0, 1, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0]] Turno: 9 Jugador: 2 Puntos faltantes: 1
¿Cuál es tu movimiento? (0-6) 2
[[0, 2, 2, 1, 2, 0, 0],
 [0, 0, 1, 2, 0, 0, 0],
 [0, 0, 1, 0, 0, 0, 0],
 [0, 0, 1, 0, 0, 0, 0],
 [0, 0, 2, 0, 0, 0, 0],
 [0, 0, 1, 0, 0, 0, 0]] Turno: 11 Jugador: 2 Puntos faltantes: 1
¿Cuál es tu movimiento? (0-6) 4
[[0, 2, 2, 1, 2, 0, 0],
 [0, 0, 1, 2, 2, 0, 0],
 [0, 0, 1, 0, 0, 0, 0],
 [0, 0, 1, 0, 0, 0, 0],
 [0, 0, 2, 0, 0, 0, 0],
 [0, 0, 1, 0, 0, 0, 0]] Turno: 13 Jugador: 2 Puntos faltantes: 2
¿Cuál es tu movimiento? (0-6) 4
[[0, 2, 2, 1, 2, 0, 0],
 [0, 1, 1, 2, 2, 0, 0],
 [0, 0, 1, 0, 1, 0, 0],
 [0, 0, 1, 0, 2, 0, 0],
 [0, 0, 2, 0, 0, 0, 0],
 [0, 0, 1, 0, 0, 0, 0]] Turno: 15 Jugador: 2 Puntos faltantes: 1
¿Cuál es tu jugada? (0-6) 3
[[0, 2, 2, 1, 2, 0, 0],
 [0, 1, 1, 2, 2, 0, 0],
 [0, 0, 1, 2, 1, 0, 0],
 [0, 0, 1, 0, 2, 0, 0],
 [0, 0, 2, 0, 0, 0, 0],
 [0, 0, 1, 0, 0, 0, 0]] Turno: 17 Jugador: 2 Puntos faltantes: 2
¿Cuál es tu jugada? (0-6) 5
[[0, 2, 2, 1, 2, 1, 1],
 [0, 1, 1, 2, 2, 2, 1],
 [0, 0, 1, 2, 1, 0, 0],
 [0, 0, 1, 0, 2, 0, 0],
 [0, 0, 2, 0, 0, 0, 0],
 [0, 0, 1, 0, 0, 0, 0]] Turno: 19 Jugador: 2 Puntos faltantes: 0
¿Cuál es tu movimiento? (0-6) 
¿Cuál es tu jugada? (0-6) 6
[[0, 2, 2, 1, 2, 1, 1],
 [0, 1, 1, 2, 2, 2, 1],
 [0, 0, 1, 2, 1, 0, 2],
 [0, 0, 1, 0, 2, 0, 0],
 [0, 0, 2, 0, 0, 0, 0],
 [0, 0, 1, 0, 0, 0, 0]] Turno: 21 Jugador: 2 Puntos faltantes: 1
¿Cuál es tu movimiento? (0-6) 1
[[0, 2, 2, 1, 2, 1, 1],
 [0, 1, 1, 2, 2, 2, 1],
 [0, 2, 1, 2, 1, 0, 2],
 [0, 1, 1, 0, 2, 0, 0],
 [0, 0, 2, 0, 0, 0, 0],
 [0, 0, 1, 0, 0, 0, 0]] Turno: 23 Jugador: 2 Puntos faltantes: 1
¿Cuál es tu jugada? (0-6) 3
[[0, 2, 2, 1, 2, 1, 1],
 [0, 1, 1, 2, 2, 2, 1],
 [0, 2, 1, 2, 1, 0, 2],
 [0, 1, 1, 2, 2, 0, 0],
 [0, 0, 2, 0, 0, 0, 0],
 [0, 0, 1, 0, 0, 0, 0]] Turno: 25 Jugador: 2 Puntos faltantes: 2
¿Cuál es tu jugada? (0-6) 6
[[0, 2, 2, 1, 2, 1, 1],
 [0, 1, 1, 2, 2, 2, 1],
 [0, 2, 1, 2, 1, 0, 2],
 [0, 1, 1, 2, 2, 0, 2],
 [0, 0, 2, 1, 0, 0, 0],
 [0, 0, 1, 1, 0, 0, 0]] Turno: 27 Jugador: 2 Puntos faltantes: 1
¿Cuál es tu jugada? (0-6) 5
[[1, 2, 2, 1, 2, 1, 1],
 [1, 1, 1, 2, 2, 2, 1],
 [0, 2, 1, 2, 1, 2, 2],
 [0, 1, 1, 2, 2, 0, 2],
 [0, 0, 2, 1, 0, 0, 0],
 [0, 0, 1, 1, 0, 0, 0]] Turno: 29 Jugador: 2 Puntos faltantes: 0
¿Cuál es tu jugada? (0-6) 5
[[1, 2, 2, 1, 2, 1, 1],
 [1, 1, 1, 2, 2, 2, 1],
 [0, 2, 1, 2, 1, 2, 2],
 [0, 1, 1, 2, 2, 2, 2],
 [0, 0, 2, 1, 0, 0, 0],
 [0, 0, 1, 1, 0, 0, 0]] Turno: 29 Ganador: 2
Ronda 1 de 1

Nombre Dibuja Pérdidas Victorias Puntuación
manual_bot: 0 0 2 1.000 zsani_bot_2: 0 2 0 -1.000

Syfer Polski
fuente