部分可观察的Connect-4


8

游戏

您将玩(几乎)Connect-4的标准游戏。不幸的是,这是一个对应游戏,有人从底部开始在第二行上放了黑色胶带,因此您在这些行中看不到对手的任何举动。

在已经满满的栏中进行的任何移动都将视为您的回合,如果游戏进行的时间超过6 * 7回合,则将其视为平局。

挑战规格

您的程序应实现为Python 3函数。第一个参数是棋盘的“视图”,代表已知棋盘的状态,是从下到上的2D行列表,其中1第一个玩家2移动,第二个玩家移动,0空位置或隐藏被你的对手移动。

第二个参数是从索引的转码0,其奇偶性告诉您您是哪个玩家。

最后一个参数是任意状态,初始化为None每个游戏开始时的状态,您可以使用该状态来保留回合之间的状态。

您应该返回要播放的列索引的2元组,并在下一轮返回新状态。

计分

胜利计为+1,平局计为,0损失计为-1。您的目标是在循环锦标赛中获得最高的平均分数。我会尝试根据需要进行尽可能多的比赛,以确定明确的获胜者。

规则

任何竞争者一次最多只能拥有一个竞争机器人,但是如果您进行了改进,可以更新您的参赛作品。请尝试将您的漫游器限制为每转大约1秒的思考时间。

测试中

这是控制器的源代码,以及一些非竞争性示例bot供参考:

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)

祝您一切顺利!

临时结果

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 

您能解释一下为什么查看view [-1] [j] == 0吗?我不完全确定我看到您在哪里填充它们,而我的python知识似乎有点生锈。
野蛮人

@ Barbarian772我正在检查该列中是否还有空间。请注意,共有6行,因此最上面一行是完整观察到的。
user1502040

1
您不应该将放置在已满的列中视为通过。许多连接4个游戏的结束时只有一栏未填充,如果一位玩家在该列中输了钱,则这将使游戏平局,否则这将迫使一位玩家获胜。
soktinpk

@soktinpk难道不只是增加另一层策略吗?毕竟Connect-4是一款已解决的游戏,因此转弯跳过因素足以满足规则更改的要求,而贡献者不能仅使用标准算法。
mypetlion

1
从底部为零索引,是粘贴的行(0,2,4,6)还是(1,3,5)?某些ASCII艺术将有所帮助。
SIGSTACKFAULT

Answers:


6

这个机器人会取得所有确定的胜利,然后退回以阻止对手,第二次在垂直和水平方向上猜测他们或随机移动。

导入pprint,数学,收藏,复制
def zsani_bot_2(视图,转弯,状态):
    如果状态==无:#第一个回合-总是在中间
        状态=(1,2)如果turn == 0否则(2,1)#(my_symbol,您的符号)
        #print(pprint.pformat(view)+'Turn:'+ str(turn)+'Player:'+ str(状态[0]))
        返回3,状态

    #找到明显的点
    对于范围在(1,6)中的我:#跳过第一行
        对于范围内的j(len(view [i])):#TODO:使用zip进行优化。现在去澄清
            如果view [i] [j]!= 0和view [i-1] [j] == 0:
                视图[i-1] [j] =状态[1]
    敌对点= math.floor(turn / 2)
    ++ enemy_points,如果state [0] == 2,否则
    known_points = sum([i中的i.count(state [1])]]
    missing_points =敌对点-known_points

    #确保在任何方向上都获胜
    对于范围(0,7)中的j:#每列
        对于范围在(4,-1,-1)中的i:
            如果view [i] [j]!= 0:
                打破#找到已知的最高填充点
        如果(不是{1、3、5}中的missing_points或i + 1):
            view1 = copy.deepcopy(视图)
            尝试= apply_move(view1,state [0],j)
            如果尝试==赢了:
               #print(pprint.pformat(view)+'Turn:'+ str(turn)+'Player:'+ str(state [0])+'获胜者移动')
                返回j,状态

    #阻止敌人在任何方向上获胜
    对于范围(0,7)中的j:
        对于范围在(4,-1,-1)中的i:
            如果view [i] [j]!= 0:
                打破#找到已知的最高填充点
        如果(不是missing_points或({1,3,5}中的i + 1)):
            view1 = copy.deepcopy(视图)
            尝试= apply_move(view1,state [1],j)
            如果尝试==赢了:
              #print(pprint.pformat(view)+'Turn:'+ str(turn)+'Player:'+ str(state [0])+'save move')
                返回j,状态

    #堵墙
    对于范围(0,3)中的i:#当列已满时不可能连续获得4
        对于范围(0,6)中的j:
            如果view [i] [j]!= 0和view [i] [j] == view [i + 1] [j]和view [i + 2] [j] == view [i + 3] [j ] == 0:
             #print(pprint.pformat(view)+'Turn:'+ str(turn)+'Player:'+ str(state [0])+'column move')
                返回j,状态

    #block平台是否在下面的行和放置点上显示完美的信息
    对于范围(0,5)中的i:
        对于范围(0,3)中的j:
            stats = collections.Counter([view [i] [j],view [i] [j + 1],view [i] [j + 2],view [i] [j + 3]])
            如果stats [0] == 2和(stats [state [0]] == 2或stats [state [0]] == 2):
                对于范围(0,3)中的k:
                    如果view [i] [j + k] == 0:
                        打破
                if(i == 0或view [i-1] [j + k]!= 0)和(不是missing_points或{1,3,5}中的i):
                    #print(pprint.pformat(view)+'Turn:'+ str(turn)+'Player:'+ str(state [0])+'platform move')
                    返回j + k,状态
                其他:
                    对于范围为(k,3)的l:
                        如果view [i] [j + 1] == 0:
                            打破
                        if(i == 0或view [i-1] [j + 1]!= 0)和(不是missing_points或{1,3,5}中的i):
                     #print(pprint.pformat(view)+'Turn:'+ str(turn)+'Player:'+ str(state [0])+'platform move')
                            返回j + 1,状态

    #fallback->随机
    而True:
        j = random.randrange(0,7)
        如果view [-1] [j] == 0:
            #print(pprint.pformat(view)+'Turn:'+ str(turn)+'Player:'+ str(state [0])+'随机移动')
            返回j,状态

感谢您修复run_game!

变更日志:

  • v2增加了水平格挡-如果在连续4行中有两个空位并且同一位玩家填充了两个位,则它将尝试填充其中一个以连续容纳3个/阻挡对手行,这有望在以下几轮中被利用。

3
欢迎来到该网站。我投票否决了将代码更改为代码的修改,最好留下评论,以便OP可以决定如何处理代码。
Ad Hoc Garf Hunter '18 -10-8

我的信誉不足,无法在主要帖子上发表评论。如何撤消编辑?
Syfer Polski

无需撤消编辑(无论如何,我认为您不能这样做)。将来发表评论就足够了,但是由于您在回答中已经说过,因此OP很可能会看到。另外,我认为 OP将拒绝您的建议并进行编辑。
Ad Hoc Garf Hunter

我想撤消修改的原因是因为我错过了更改中的一行,否则,修改后的代码将完全无法运行。我已将其包含在帖子的编辑建议中。感谢您的帮助!
Syfer Polski

2

normalBot假设中间的点比两端的点更有价值。因此,它使用居中的正态分布来确定其选择。

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

-1

这显然是非竞争性的,但是对于调试非常有用...而且令人惊讶地很有趣,如果您知道您的机器人足以欺骗:D,那么我发帖希望能够激发更多的提交意见:

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

网格是上下颠倒的(最下面一行)。要获得获胜者公告,您需要修补游戏控制器,并在返回获胜之前添加打印声明:

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]]转:0玩家:1失分:0
您的举动是什么?(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]]回合:2玩家:1失分:0
您的举动是什么?(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]]转:4玩家:1失分:1
您的举动是什么?(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]]转:6玩家:1失分:2
您的举动是什么?(0-6)1
[[2,1,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]]转:6获胜者: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]]转:1玩家:2失分:1
您的举动是什么?(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]]转:3玩家:2失分:2
您的举动是什么?(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]]回合:5玩家:2失分:1
您的举动是什么?(0-6)4
[[0,0,2,1,2,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]]回合:7玩家:2失分:2
您的举动是什么?(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]]转:9玩家:2失分:1
您的举动是什么?(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]]转:11玩家:2失分:1
您的举动是什么?(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]]转:13玩家:2失分:2
您的举动是什么?(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]]转:15玩家:2失分:1
您的举动是什么?(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]]转:17玩家:2失分:2
您的举动是什么?(0-6)5
[[0,2,2,1,2,1,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]]转:19玩家:2失分:0
您的举动是什么?(0-6) 
您的举动是什么?(0-6)6
[[0,2,2,1,2,1,1,1],
 [0,1,1,2,2,2,1]
 [0,0,1,2,1,1,0,2],
 [0,0,1,0,2,0,0],
 [0,0,2,0,0,0,0],
 [0,0,1,0,0,0,0]]转:21玩家:2失分:1
您的举动是什么?(0-6)1
[[0,2,2,1,2,1,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]]转:23玩家:2失分:1
您的举动是什么?(0-6)3
[[0,2,2,1,2,1,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]]转:25玩家:2失分:2
您的举动是什么?(0-6)6
[[0,2,2,1,2,1,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]]转:27玩家:2失分:1
您的举动是什么?(0-6)5
[[1,2,2,1,2,1,1,1]
 [1、1、1、2、2、2、1],
 [0,2,1,2,1,1,2,2],
 [0,1,1,2,2,0,2],
 [0,0,2,1,0,0,0],
 [0,0,1,1,0,0,0]]转:29玩家:2失分:0
您的举动是什么?(0-6)5
[[1,2,2,1,2,1,1,1]
 [1、1、1、2、2、2、1],
 [0,2,1,2,1,1,2,2],
 [0,1,1,2,2,2,2],
 [0,0,2,1,0,0,0],
 [0,0,1,1,0,0,0]]转:29获胜者:2
第1轮,共1轮

名字吸引损失获胜得分
manual_bot:0 0 2 1.000 zsani_bot_2:0 2 0 -1.000

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.