用洛書幻方對抗人類玩家的井字棋程式

劉新宇發表於2020-05-01

我們在此前的文章中,給出了一個練習題:

程式設計實現一個井字棋遊戲是傳統人工智慧中的經典問題,而計算機可以輕鬆算出三個數字的和並判斷其是否等於15。請利用這個同構編寫一個簡化的井字棋程式,並做到不被人類玩家擊敗。

現在我們給出這道題目的參考答案。我們的思路是使用《洛書》幻方來同構井字棋遊戲,用集合X, O來儲存兩個玩家所佔領的格子。對於春節廟會中描述的對局,開始時X = [],O = [],結束時X = [2, 5, 7, 1 ],Y = [ 4, 3, 8, 6 ]。為此我們需要先寫一個程式判斷一個集合中是否有3個元素相加等於15,從而知道某個玩家獲勝與否。

有兩種思路解決這個問題,第一種是列舉洛書幻方中的所有行、列、對角線,共8個三元組:[[4, 9, 2], [3, 5, 7], ..., [2, 5, 8]]。然後看是否某個三元組包含在玩家佔領的格子集合中。第二種比較有趣,假設玩家佔領了格子 X = [x_1, x_2, ..., x_n]。這些格子按照洛書幻方中的元素升序排列。我們可以先選出x_1,然後用左右兩個指標l, r分別指向下一個元素和最後一個元素,然後把這3個數加起來s = x_1 + x_l + x_r,如果等於15,說明玩家連成一條直線已經獲勝了。如果小於15,由於元素是升序排列的,我們可以把左側指標l加一,然後再次嘗試;如果大於15,我們把右側指標r減一,然後再次嘗試。如果左右指標相遇,說明固定x_1沒有找到相加等於15的三元組,我們選出x_2再次進行這樣的檢查。這樣最差情況總共進行(n - 2)+ (n - 3) + ... + 1次檢查就得知玩家是否獲勝了。

def win(s):
    n = len(s)
    if n < 3:
        return False
    s = sorted(s)
    for i in range(n - 2):
        l = i + 1
        r = n - 1
        while l < r:
            total = s[i] + s[l] + s[r]
            if total == 15:
                return True
            elif total < 15:
                l = l + 1
            else:
                r = r - 1
    return False

這樣給定X和O,就能判斷局面。如果X和O佔滿全部9個格子,還未分出勝負,則表示平局。接下來我們用傳統人工智慧中的min-max方法來實現井字棋,我們給每個局面一個評分,一方試圖讓評分最大化,稱為正方;另一方試圖讓評分最小化,稱為反方,從而實現對抗。平局的話評分為0,如果某個局面讓正方獲勝,我們設定評分為10,反方獲勝評分為-10。這個分數值完全是隨意設定的,不影響結果。

WIN = 10
INF = 1000

# Luo Shu magic square
MAGIC_SQUARE = [4, 9, 2,
                3, 5, 7,
                8, 1, 6]

def eval(x, o):
    if win(x):
        return WIN
    if win(o):
        return -WIN
    return 0

def finished(x, o):
    return len(x) + len(o) == 9

對於任何一個對局,我們都讓計算機不斷向前探索,直到找到輸贏或者平局的確定局面才停下來。探索的方法是窮盡當前所有能佔領的格子,然後轉換身份,考慮自己是對方時怎樣對抗。對於所有候選方案,如果是正方,就選擇評分高的方案,如果是反方,就選擇評分低的方案。

def findbest(x, o, maximize):
    best = -INF if maximize else INF
    move = 0
    for i in MAGIC_SQUARE:
        if (i not in x) and (i not in o):
            if maximize:
                val = minmax([i] + x, o, 0, not maximize)
                if val > best:
                    best = val
                    move = i
            else:
                val = minmax(x, [i] + o, 0, not maximize)
                if val < best:
                    best = val
                    move = i
    return move

min-max是一個遞迴搜尋的過程,為了儘快獲勝,我們在評分上加上對向前探索步數的考慮。如果是正方,就從評分中減去遞迴深度,而對於反方,則加上遞迴深度。

def minmax(x, o, depth, maximize):
    score = eval(x, o)
    if score == WIN:
        return score - depth
    if score == -WIN:
        return score + depth
    if finished(x, o):
        return 0  # draw
    best = -INF if maximize else INF
    for i in MAGIC_SQUARE:
        if (i not in x) and (i not in o):
            if maximize:
                best = max(best, minmax([i] + x, o, depth + 1, not maximize))
            else:
                best = min(best, minmax(x, [i] + o, depth + 1, not maximize))
    return best

現在我們就做出一個無法被人類擊敗的程式了,我們的程式在背後用洛書幻方對抗人類玩家:

def board(x, o):
    for r in range(3):
        print("-------")
        for c in range(3):
            p = MAGIC_SQUARE[r*3 + c]
            if p in x:
                print("|X", end="")
            elif p in o:
                print("|O", end="")
            else:
                print("| ", end="")
        print("|")
    print("-------")

def play():
    x = []
    o = []
    while not (win(x) or win(o) or finished(x, o)):
        board(x, o)
        while True:
            i = int(input("[1..9]==>"))
            if i not in MAGIC_SQUARE or MAGIC_SQUARE[i-1] in x or MAGIC_SQUARE[i-1] in o:
                print("invalid move")
            else:
                x = [MAGIC_SQUARE[i-1]] + x
                break
        o = [findbest(x, o, False)] + o
    board(x, o)

這是《同構——程式設計中的數學》一書前言中的練習題

相關文章