Partially-Observable Connect-4

This bot takes all sure wins, and falls back to block the rivals, second guess them vertically and horizontally or make random moves.

import pprint, math, collections, copy
def zsani_bot_2(view, turn, state):
    if state == None: #first own turn - always for for middle
        state = (1, 2) if turn == 0 else (2, 1) #(my_symbol, your symbol)
        #print(pprint.pformat(view) + ' Turn: ' + str(turn) + ' Player: ' + str(state[0]))
        return 3, state

    #locate obvious points
    for i in range (1, 6):              #skip first row
        for j in range(len(view[i])):   #TODO: Optimise with zip. Go for clarity now
            if view[i][j] != 0 and view[i-1][j] == 0:
                view[i-1][j] = state[1]
    enemy_points = math.floor(turn/2)
    ++enemy_points if state[0] == 2 else enemy_points
    known_points = sum([i.count(state[1]) for i in view])
    missing_points = enemy_points - known_points

    #get sure wins in any direction
    for j in range(0, 7): #every column
        for i in range(4, -1, -1):
            if view[i][j] !=0:
                break #find highest known filled point
        if (not missing_points or i+1 in {1, 3, 5}):
            view1 = copy.deepcopy(view)
            attempt = apply_move(view1, state[0], j)
            if attempt == WON:
               # print(pprint.pformat(view) + ' Turn: ' + str(turn) + ' Player: ' + str(state[0]) + ' winner move')
                return j, state

    #block sure enemy wins in any direction
    for j in range(0, 7):
        for i in range(4, -1, -1):
            if view[i][j] !=0:
                break #find highest known filled point
        if (not missing_points or (i+1 in {1, 3, 5})):
            view1 = copy.deepcopy(view)
            attempt = apply_move(view1, state[1], j)
            if attempt == WON:
              #  print(pprint.pformat(view) + ' Turn: ' + str(turn) + ' Player: ' + str(state[0]) + ' saving move')
                return j, state

    #block walls
    for i in range(0, 3): #impossible to get 4 in a row when the column is full
        for j in range(0, 6):
            if view[i][j] != 0 and view[i][j] == view[i+1][j] and view[i+2][j] == view[i+3][j] == 0:
             #   print(pprint.pformat(view) + ' Turn: ' + str(turn) + ' Player: ' + str(state[0]) + ' column move')
                return j, state

    #block platforms if posessing perfect information on row below and drop point
    for i in range(0, 5):
        for j in range(0, 3):
            stats = collections.Counter([view[i][j], view[i][j+1], view[i][j+2], view[i][j+3]])
            if stats[0] == 2 and (stats[state[0]] == 2 or stats[state[0]] == 2):
                for k in range(0, 3):
                    if view[i][j+k] == 0:
                        break
                if (i == 0 or 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')
                    return j+k, state
                else:
                    for l in range (k, 3):
                        if view[i][j+l] == 0:
                            break
                        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')
                            return j+l, state

    #fallback -> random
    while True:
        j = random.randrange(0, 7)
        if view[-1][j] == 0:
            #print(pprint.pformat(view) + ' Turn: ' + str(turn) + ' Player: ' + str(state[0]) + ' random move')
            return j, state

Thank you for fixing run_game!

Changelog:

  • v2 adds horizontal blocking - if, in a row of 4, there are two empty spots and two spots filled by the same player, it will attempt to fill one of them to have three in a row/block the opponents row, which will hopefully be capitalized upon in the following turns.

normalBot plays upon the assumption that spots in the middle are more valuable than spots on the ends. Thus, it uses a normal distribution centered in the middle to determine its choices.

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