Connect-4 partiellement observable

8

Le jeu

Vous jouerez à un jeu (presque) standard de Connect-4 . Malheureusement, c'est un jeu de correspondance et quelqu'un a placé du ruban noir sur chaque deuxième rangée en partant du bas, de sorte que vous ne pouvez voir aucun des mouvements de votre adversaire dans ces rangées.

Tout mouvement dans des colonnes déjà pleines comptera comme passant votre tour, et si un jeu dure plus longtemps que les 6 * 7tours, il sera jugé comme un match nul.

Spécifications du défi

Votre programme doit être implémenté en tant que fonction Python 3. Le premier argument est une «vue» du plateau, représentant l'état connu du plateau sous la forme d'une liste 2D de lignes de bas en haut où se 1trouve un mouvement du premier joueur, 2un mouvement du deuxième joueur, et 0une position vide ou cachée bougez par votre adversaire.

Le deuxième argument est un numéro de tour indexé 0, et sa parité vous indique de quel joueur vous êtes.

Le dernier argument est un état arbitraire, initialisé Noneau début de chaque partie, que vous pouvez utiliser pour conserver l'état entre les tours.

Vous devez retourner un 2-tuple de l'index de la colonne que vous souhaitez jouer, et le nouvel état à vous rendre au prochain tour.

Notation

Une victoire compte pour +1, un nul pour 0et une perte pour -1. Votre objectif est d'atteindre le score moyen le plus élevé dans un tournoi à la ronde. J'essaierai d'organiser autant de matchs que nécessaire pour identifier un vainqueur clair.

Règles

Tout concurrent doit avoir au plus un bot concurrent à la fois, mais il est OK de mettre à jour votre inscription si vous apportez des améliorations. Veuillez essayer de limiter votre bot à environ 1 seconde de temps de réflexion par tour.

Essai

Voici le code source du contrôleur, avec quelques exemples de robots non concurrents pour référence:

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)

Joyeux KoTHing!

Résultats provisoires

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
la source
Pourriez-vous expliquer pourquoi vous vérifiez la vue [-1] [j] == 0? Je ne suis pas tout à fait sûr de voir où vous les avez remplis et mes connaissances en python semblent être un peu rouillées.
Barbarian772
@ Barbarian772 Je vérifie s'il reste de l'espace dans cette colonne. Notez qu'il y a 6 lignes afin que la ligne du haut soit entièrement observée.
user1502040
1
Vous ne devez pas compter le placement dans des colonnes déjà pleines comme un passage. Beaucoup se connectent 4 parties se terminent avec une seule colonne non remplie et si un joueur perd en jouant dans cette colonne, cela rendra le match nul quand il s'agit autrement d'une victoire forcée pour un joueur.
soktinpk
@soktinpk Cela n'ajoute-t-il pas simplement une autre couche de stratégie? Connect-4 est un jeu résolu après tout, donc le facteur de saut de tour pourrait être assez d'un changement de règle pour que les contributeurs ne puissent pas simplement utiliser les algorithmes standard.
mypetlion
1
Indexation zéro à partir du bas, les lignes enregistrées sont-elles (0,2,4,6) ou (1,3,5)? Un certain art ASCII serait utile.
SIGSTACKFAULT

Réponses:

6

Ce bot prend toutes les victoires, et recule pour bloquer les rivaux, les deviner ensuite verticalement et horizontalement ou faire des mouvements aléatoires.

importer pprint, mathématiques, collections, copier
def zsani_bot_2 (voir, tourner, indiquer):
    si état == Aucun: #premier tour propre - toujours pour le milieu
        state = (1, 2) if turn == 0 else (2, 1) # (my_symbol, your symbol)
        #print (pprint.pformat (view) + 'Turn:' + str (turn) + 'Player:' + str (state [0]))
        retour 3, état

    # localiser les points évidents
    pour i dans la plage (1, 6): # sauter la première ligne
        pour j dans la plage (len (vue [i])): #TODO: Optimiser avec zip. Optez pour la clarté maintenant
            si afficher [i] [j]! = 0 et afficher [i-1] [j] == 0:
                afficher [i-1] [j] = état [1]
    ennemi_points = math.floor (tour / 2)
    ++ ennemi_points si état [0] == 2 sinon ennemi_points
    points_connus = somme ([i.count (état [1]) pour i en vue])
    points_manquants = points_ennemis - points_connus

    # soyez sûr de gagner dans n'importe quelle direction
    pour j dans la plage (0, 7): # chaque colonne
        pour i dans la plage (4, -1, -1):
            si vue [i] [j]! = 0:
                break #find le plus haut point de remplissage connu
        if (not missing_points or i + 1 in {1, 3, 5}):
            view1 = copy.deepcopy (voir)
            tentative = appliquer_move (vue1, état [0], j)
            si tentative == GAGNÉ:
               # print (pprint.pformat (view) + 'Turn:' + str (turn) + 'Player:' + str (state [0]) + 'move du gagnant')
                retour j, état

    #block sûr que l'ennemi gagne dans n'importe quelle direction
    pour j dans la plage (0, 7):
        pour i dans la plage (4, -1, -1):
            si vue [i] [j]! = 0:
                break #find le plus haut point de remplissage connu
        if (not missing_points or (i + 1 in {1, 3, 5})):
            view1 = copy.deepcopy (voir)
            tentative = appliquer_move (vue1, état [1], j)
            si tentative == GAGNÉ:
              # print (pprint.pformat (view) + 'Turn:' + str (turn) + 'Player:' + str (state [0]) + 'save move')
                retour j, état

    # murs de blocs
    pour i dans la plage (0, 3): #impossible d'obtenir 4 de suite lorsque la colonne est pleine
        pour j dans la plage (0, 6):
            si afficher [i] [j]! = 0 et afficher [i] [j] == afficher [i + 1] [j] et afficher [i + 2] [j] == afficher [i + 3] [j ] == 0:
             # print (pprint.pformat (view) + 'Turn:' + str (turn) + 'Player:' + str (state [0]) + 'column move')
                retour j, état

    # bloquez les plateformes si vous possédez des informations parfaites sur la ligne ci-dessous et le point de chute
    pour i dans la plage (0, 5):
        pour j dans la plage (0, 3):
            stats = collections.Counter ([afficher [i] [j], afficher [i] [j + 1], afficher [i] [j + 2], afficher [i] [j + 3]])
            si stats [0] == 2 et (stats [état [0]] == 2 ou stats [état [0]] == 2):
                pour k dans la plage (0, 3):
                    si vue [i] [j + k] == 0:
                        Pause
                if (i == 0 ou view [i-1] [j + k]! = 0) and (not missing_points or i in {1, 3, 5}):
                    #print (pprint.pformat (view) + 'Turn:' + str (turn) + 'Player:' + str (state [0]) + 'platform move')
                    retour j + k, état
                autre:
                    pour l dans la plage (k, 3):
                        si vue [i] [j + l] == 0:
                            Pause
                        if (i == 0 or view [i-1] [j + l]! = 0) and (not missing_points or i in {1, 3, 5}):
                     # print (pprint.pformat (view) + 'Turn:' + str (turn) + 'Player:' + str (state [0]) + 'platform move')
                            retour j + l, état

    #fallback -> random
    tandis que True:
        j = random.randrange (0, 7)
        si vue [-1] [j] == 0:
            #print (pprint.pformat (view) + 'Turn:' + str (turn) + 'Player:' + str (state [0]) + 'random move')
            retour j, état

Merci d'avoir réparé run_game!

Journal des modifications:

  • v2 ajoute un blocage horizontal - si, dans une rangée de 4, il y a deux emplacements vides et deux emplacements remplis par le même joueur, il tentera de remplir l'un d'eux pour en avoir trois d'affilée / bloquer la ligne de l'adversaire, ce qui, espérons-le, être capitalisé dans les tours suivants.
Syfer Polski
la source
3
Bienvenue sur le site. J'ai voté pour rejeter la modification pour passer au code, il serait préférable de laisser un commentaire, de cette façon, l'OP peut décider quoi faire avec le code.
Ad Hoc Garf Hunter
Je n'ai pas assez de réputation pour commenter le post principal. Comment retirer une modification?
Syfer Polski du
Pas besoin de retirer le montage (je ne pense pas que vous puissiez de toute façon). À l'avenir, les commentaires seront suffisants, mais comme vous l'avez dit dans votre réponse, il est probable que le PO verra. De plus, je pense que l'OP verra que vous avez suggéré et édité même s'il a été rejeté.
Ad Hoc Garf Hunter
La raison pour laquelle j'aimerais retirer la modification est parce que j'ai manqué une ligne dans les modifications, sans laquelle le code modifié ne fonctionnera pas complètement. Je l'ai inclus dans la suggestion de modification de mon message. Merci de votre aide!
Syfer Polski
2

normalBot joue sur l'hypothèse que les taches au milieu ont plus de valeur que les taches aux extrémités. Ainsi, il utilise une distribution normale centrée au milieu pour déterminer ses choix.

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
la source
-1

C'est évidemment non compétitif, mais néanmoins très utile pour le débogage ... et étonnamment amusant, si vous connaissez assez bien votre bot pour tricher: D donc je poste dans l'espoir d'inspirer plus de soumissions:

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 grille est à l'envers (la rangée du bas est la plus haute). Pour obtenir les annonces des gagnants, vous devez patcher le contrôleur de jeu, en ajoutant une déclaration d'impression avant de retourner la victoire:

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]] Tour: 0 Joueur: 1 Points manquants: 0
Quelle est votre décision? (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]] Tour: 2 Joueur: 1 Points manquants: 0
Quelle est votre décision? (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]] Tour: 4 Joueur: 1 Points manquants: 1
Quelle est votre décision? (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]] Tour: 6 Joueur: 1 Points manquants: 2
Quelle est votre décision? (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]] Tour: 6 Gagnant: 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]] Tour: 1 Joueur: 2 Points manquants: 1
Quelle est votre décision? (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]] Tour: 3 Joueur: 2 Points manquants: 2
Quelle est votre décision? (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]] Tour: 5 Joueur: 2 Points manquants: 1
Quelle est votre décision? (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]] Tour: 7 Joueur: 2 Points manquants: 2
Quelle est votre décision? (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]] Tour: 9 Joueur: 2 Points manquants: 1
Quelle est votre décision? (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]] Tour: 11 Joueur: 2 Points manquants: 1
Quelle est votre décision? (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]] Tour: 13 Joueur: 2 Points manquants: 2
Quelle est votre décision? (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]] Tour: 15 Joueur: 2 Points manquants: 1
Quelle est votre décision? (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]] Tour: 17 Joueur: 2 Points manquants: 2
Quelle est votre décision? (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]] Tour: 19 Joueur: 2 Points manquants: 0
Quelle est votre décision? (0-6) 
Quelle est votre décision? (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]] Tour: 21 Joueur: 2 Points manquants: 1
Quelle est votre décision? (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]] Tour: 23 Joueur: 2 Points manquants: 1
Quelle est votre décision? (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]] Tour: 25 Joueur: 2 Points manquants: 2
Quelle est votre décision? (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]] Tour: 27 Joueur: 2 Points manquants: 1
Quelle est votre décision? (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]] Tour: 29 Joueur: 2 Points manquants: 0
Quelle est votre décision? (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]] Tour: 29 Gagnant: 2
Tour 1 de 1

Nom Tirages Défaites Victoires Score
manual_bot: 0 0 2 1.000 zsani_bot_2: 0 2 0 -1.000

Syfer Polski
la source