python 五子棋AI實現(3):極大極小值搜索和alpha beta剪枝
極大極小值搜索介紹
可以先回顧下上一篇中的AI 實現:AI 先獲取當前所有可以下的位置(就是棋盤上的空格),然後每次在其中一個位置下子,根據棋型評估函數獲取一個分數,所有位置都下過一遍後,從中獲取評分最高的位置。這個就是極大值的搜索過程,我們稱爲 AI 的MAX層,即AI 要保證自己下棋的評分最大化。如果是輪到玩家下棋時,肯定會選取對自己最有利的位置,也可以說是對AI最不利的位置,即評分要最小化,我們稱爲AI的MIN層。。
上面是隻有一層的搜索,如果要考慮多層搜索,第一層是AI下棋,第二層是玩家下棋,第三層是AI下棋,第四層是玩家下棋,依次類推。假設每一層有50個可選擇的位置,每個位置看做樹的一個節點,那麼第一層是根節點下面的子節點,有50個節點,第二層是第一層下面的子節點,就有50×50個節點,第三層就有50×50×50個節點,依次類推,這樣會形成一個巨大的博弈樹。我們要做的就是搜索這棵樹,找到對於AI最有利的下棋位置。
假設一個兩層的博弈樹,如圖1所示,最上面一層是樹的根節點,這裏MAX表示會選取下一層子節點中評分最高的。第二層的MIN表示會選取下一層子節點中評分最低的。第三層是葉子節點,只需要計算評分。注意:只有在葉子節點時纔會計算評分,在樹的中間層,對於AI來說暫時是無法知道哪一個節點是最有利的。
圖1
極大極小值搜索是一個深度優先的算法,當第二層第一個節點的子節點都計算好評分後,因爲這層是MIN層,會選取子節點中最低的評分作爲這個節點的評分,就是3。依次類推,第二層第二個節點評分爲6,第三個節點爲5。當第二層節點都獲取到評分後,因爲第一層是MAX層,會選取子節點中最高的評分作爲這個節點的評分,就是第二層第二個節點的評分6,這個節點所代表的下棋位置對於AI來說就是最有利的。最後算法返回評分6和第二層第二個節點的下棋位置。
根節點表示當前的棋局,博弈樹上每一個節點就是從樹根節點開始,每個子節點下一步棋,到該節點形成的新的棋局。 棋局就是所有已下棋位置的順序列表,比如[ (7,7), (8,8), (7,9) ]。
圖2
比如圖2的兩層的博弈樹,一開始玩家下了一步棋,位置是(7,7), 輪到AI下棋,AI進行博弈樹搜索時,根節點已有的棋局就是 [ (7,7) ]。第二層時AI下棋,假設AI選擇了三個下棋的位置(8,7),(8,8),(7,6),就形成了第二層的三個節點,這三個節點分別代表三個新的棋局,[ (7,7), (8,7) ], [ (7,7), (8,8) ], [ (7,7), (7,6) ]。 第三層輪到玩家下棋,看下中間評分爲6的葉子節點,選擇的下棋位置是(7,9), 所以這個葉節點的棋局是 [ (7,7), (8,8), (7,9 ],葉子節點的評分就是對這個棋局用棋型評估函數進行打分。
根據棋型評估函數 先確定下評分的定義:
假設AI 是黑棋,玩家是白棋,在某一層某個節點時的評分,就是對這個節點形成的棋局的評分:(AI 黑棋棋型的評分 - 玩家白棋棋形的評分):
極大極小值搜索算法就是在樹的每一層搜索時,根據下面的策略:
- AI下棋的層,稱爲MAX層,這一層 AI 會選取子節點中評分最高的位置
- 玩家下棋的層,稱爲MIN層,這一層玩家會選取子節點中評分最低的位置
alpha beta剪枝介紹
極大極小值搜索算法的缺點就是當博弈樹的層數變大時,需要搜索的節點數目會指數級增長。比如上面每一層的節點爲50時,六層博弈樹的節點就是50的6次方,運算時間會非常漫長。
在上面的例子中,我們會計算所有葉子節點的評分,但這個不是必要的。
Alpha-Beta剪枝就是用來將搜索樹中不需要搜索的分支裁剪掉,以提高運算速度。基本的原理是:
- 當一個 MIN 層節點的 α值 ≤ β值時 ,剪掉該節點的所有未搜索子節點
- 當一個 MAX 層節點的 α值 ≥ β值時 ,剪掉該節點的所有未搜索子節點
其中α值是該層節點當前最有利的評分,β值是父節點當前的α值,根節點因爲是MAX層,所以 β值 初始化爲正無窮大(+∞)。
初始化節點的α值,如果是MAX層,初始化α值爲負無窮大(-∞),這樣子節點的評分肯定比這個值大。如果是MIN層,初始化α值爲正無窮大(+∞),這樣子節點的評分肯定比這個值小。
MIN層剪枝
我們先看一個MIN層剪枝的例子,根節點A的α, β值爲(-∞, +∞),博弈樹層數爲2。
如圖3,是開始搜索第二層第一個子節點B時的情況。因爲節點B是在MIN層,所以α, β值設爲(+∞, -∞),β值是父節點A當前的α值。
圖3
如圖4,是搜索完節點B,並更新了節點B和根節點A的α值後,開始搜索第二層第二個節點C時的情況。根節點A的α值更新爲當前最有利的評分3。節點C的α, β值設爲(+∞, 3),β值是父節點A當前的α值。
圖4
如圖5,是搜索完節點C,並更新了節點C和根節點A的α值後,開始搜索第二層第三個節點D時的情況。節點C的α值更新爲6,根節點A的α值更新爲當前最有利的評分6。節點D的α, β值設爲(+∞, 6),β值是父節點A當前的α值。
圖5
如圖6,就是MIN層剪枝的過程,搜索節點D的第一個子節點,得到的評分是5,更新節點D的α值爲5,這時節點D 符合MIN層的剪枝判斷: α值 ≤ β值,所以節點D的第二個子節點就被裁剪了。
圖6
MAX層剪枝
再看一個MAX層剪枝的例子,根節點A的α, β值爲(-∞, +∞),博弈樹層數爲4。
如圖7,是搜索完第三層第一個節點C的第一個子節點的情況。節點B是在MIN層,所以α, β值設爲(+∞, -∞),節點C是在MAX層,所以α, β值設爲(-∞,+∞)。節點C的第一個子節點初始α, β值設爲(+∞, -∞),搜索完後α值更新爲5。
圖7
如圖8,是搜索完節點C後,更新完節點B的情況,節點C的α值更新爲5, 節點B的α值同樣更新爲5。
圖8
如圖9,是開始搜索節點B的第二個子節點D時的情況。節點D在MAX層,所以α, β值設爲(-∞, 5), β值是父節點B當前的α值。
圖9
如圖10,是搜索完節點D的第一個子節點後的情況。節點D的第一個子節點的評分爲7,更新節點D的α值爲7,這時節點D 符合MAX層的剪枝判斷: α值 >= β值,所以節點D的第二個子節點就被裁剪了。
圖10
代碼實現
alpha,beta剪枝實現
注意代碼實現和上面的算法介紹有些不同: 子節點的β值是 父節點的 -α值,返回給父節點的評分是子節點的-α值。因爲按照上面的剪枝算法,MIN層和MAX層的判斷條件是不同的,爲了代碼實現的簡潔,這樣修改後就可以使用相同的選擇最有利位置的條件:MAX層 和 MIN層 都選擇最大的評分, 和相同的剪枝判斷條件:MAX層 和 MIN層 都在 α值 ≥ β值時 ,剪掉該節點的所有未搜索子節點。
比如上面圖1的例子,按照代碼實現,就變成圖11的樣子。節點B的兩個子節點的評分爲5 和 3,返回到節點B時,就變成 -5 和 -3,這時選擇最大的評分,就是 -3,對應的下棋位置和圖1中還是一樣的。節點B 和 節點D 按照同樣的規則,選擇最大的評分爲 -6 和 -5。
節點A的三個子節點的評分分別爲-3, -6 和 -5,返回到節點A時,評分就變成 3, 6 和 5,選擇最大的評分,就是6,對應的節點C和圖1還是一樣的。
圖11
主要是修改了AI的search函數,新增的__search 函數實現了 AI的深度搜索和alpha,beta剪枝。
AI_SEARCH_DEPTH 表示搜索深度,默認是2,測試時可以改成4。
AI_SEARCH_DEPTH = 2
SCORE_MAX = 0x7fffffff
SCORE_MIN = -1 * SCORE_MAX
SCORE_FIVE = 10000
def __search(self, board, turn, depth, alpha = SCORE_MIN, beta = SCORE_MAX):
score = self.evaluate(board, turn)
if depth <= 0 or abs(score) >= SCORE_FIVE:
return score
moves = self.genmove(board, turn)
bestmove = None
self.alpha += len(moves)
# if there are no moves, just return the score
if len(moves) == 0:
return score
for _, x, y in moves:
board[y][x] = turn
if turn == MAP_ENTRY_TYPE.MAP_PLAYER_ONE:
op_turn = MAP_ENTRY_TYPE.MAP_PLAYER_TWO
else:
op_turn = MAP_ENTRY_TYPE.MAP_PLAYER_ONE
score = - self.__search(board, op_turn, depth - 1, -beta, -alpha)
board[y][x] = 0
self.belta += 1
# alpha/beta pruning
if score > alpha:
alpha = score
bestmove = (x, y)
if alpha >= beta:
break
if depth == self.maxdepth and bestmove:
self.bestmove = bestmove
return alpha
def search(self, board, turn, depth = 4):
self.maxdepth = depth
self.bestmove = None
score = self.__search(board, turn, depth)
x, y = self.bestmove
return score, x, y
獲取子節點
上一篇文章中獲取子節點的方法是,直接返回當前棋盤上所有空的位置。當搜索層數是1的時候,搜索節點最多225個,搜索時間可以忽略。但是當搜索層數變成2,4時,需要搜索的節點就變成 255×255,或255×255×255×255,這個搜索時間就太長了。
注意到很多空的位置是沒有價值的,比如不能和已有的棋子形成棋型,或者擋住對方的棋型,可以直接忽略這些位置。
所以只考慮在已下雙方棋子的一定範圍內的空位置,可以考慮在範圍1內的棋子。
比如下面只下了2步棋的棋盤上,範圍1內的空位置有12個。
修改後的genmove函數,獲取已下雙方棋子範圍1內的空位置。hasNeighbor函數,判斷空位置範圍1內是否有已下的棋子。
def hasNeighbor(self, board, x, y, radius):
start_x, end_x = (x - radius), (x + radius)
start_y, end_y = (y - radius), (y + radius)
for i in range(start_y, end_y+1):
for j in range(start_x, end_x+1):
if i >= 0 and i < self.len and j >= 0 and j < self.len:
if board[i][j] != 0:
return True
return False
# get all positions near chess
def genmove(self, board, turn):
fives = []
mfours, ofours = [], []
msfours, osfours = [], []
if turn == MAP_ENTRY_TYPE.MAP_PLAYER_ONE:
mine = 1
opponent = 2
else:
mine = 2
opponent = 1
moves = []
radius = 1
for y in range(self.len):
for x in range(self.len):
if board[y][x] == 0 and self.hasNeighbor(board, x, y, radius):
score = self.pos_score[y][x]
moves.append((score, x, y))
moves.sort(reverse=True)
return moves
AI搜索深度和搜索時間
搜索深度就是博弈樹的層數,博弈樹的層數越多,AI就越厲害。但是由於搜索時間會指數級增加,所以這裏只測試了深度爲2和4的情況。
- 搜索深度爲2,搜索時間基本在1秒以內。下面時程序運行時的統計信息,這邊的alpha值可以看成是不開啓剪枝會搜索的節點數目,(alpha - beta)值可以看成是裁剪掉的節點數目。
time[0.04] (8, 8), score[-6] alpha[96] belta[28]
time[0.04] (8, 7), score[-10] alpha[271] belta[43]
time[0.09] (7, 8), score[-398] alpha[304] belta[115]
time[0.07] (9, 5), score[-402] alpha[378] belta[125]
time[0.29] (4, 10), score[-392] alpha[693] belta[566]
time[0.15] (9, 6), score[-16] alpha[1041] belta[232]
time[0.16] (10, 5), score[-20] alpha[980] belta[245]
time[0.23] (7, 5), score[-22] alpha[1388] belta[321] - 搜索深度爲4, 搜索時間不穩定,可能會到20秒。後續會優化到搜索時間在2秒左右。
time[0.73] (8, 8), score[-10] alpha[5275] belta[1388]
time[1.63] (6, 8), score[-394] alpha[11835] belta[3122]
time[1.19] (8, 7), score[-398] alpha[10195] belta[1859]
time[1.49] (7, 6), score[-400] alpha[12053] belta[2382]
time[4.27] (6, 5), score[-402] alpha[25392] belta[7005]
time[3.65] (6, 7), score[-394] alpha[35265] belta[5135]
time[5.82] (8, 5), score[-8] alpha[36939] belta[8458]
完整代碼
一共有三個文件,main.py, GameMap.py 和 ChessAI.py。這次只修改了ChessAI.py,前兩個文件可以看上兩篇文章中的代碼。
ChessAI.py
from GameMap import *
from enum import IntEnum
from random import randint
import time
AI_SEARCH_DEPTH = 2
class CHESS_TYPE(IntEnum):
NONE = 0,
SLEEP_TWO = 1,
LIVE_TWO = 2,
SLEEP_THREE = 3
LIVE_THREE = 4,
CHONG_FOUR = 5,
LIVE_FOUR = 6,
LIVE_FIVE = 7,
CHESS_TYPE_NUM = 8
FIVE = CHESS_TYPE.LIVE_FIVE.value
FOUR, THREE, TWO = CHESS_TYPE.LIVE_FOUR.value, CHESS_TYPE.LIVE_THREE.value, CHESS_TYPE.LIVE_TWO.value
SFOUR, STHREE, STWO = CHESS_TYPE.CHONG_FOUR.value, CHESS_TYPE.SLEEP_THREE.value, CHESS_TYPE.SLEEP_TWO.value
SCORE_MAX = 0x7fffffff
SCORE_MIN = -1 * SCORE_MAX
SCORE_FIVE = 10000
class ChessAI():
def __init__(self, chess_len):
self.len = chess_len
# [horizon, vertical, left diagonal, right diagonal]
self.record = [[[0,0,0,0] for x in range(chess_len)] for y in range(chess_len)]
self.count = [[0 for x in range(CHESS_TYPE_NUM)] for i in range(2)]
self.pos_score = [[(7 - max(abs(x - 7), abs(y - 7))) for x in range(chess_len)] for y in range(chess_len)]
def reset(self):
for y in range(self.len):
for x in range(self.len):
for i in range(4):
self.record[y][x][i] = 0
for i in range(len(self.count)):
for j in range(len(self.count[0])):
self.count[i][j] = 0
def click(self, map, x, y, turn):
map.click(x, y, turn)
def isWin(self, board, turn):
return self.evaluate(board, turn, True)
# check if has a none empty position in it's radius range
def hasNeighbor(self, board, x, y, radius):
start_x, end_x = (x - radius), (x + radius)
start_y, end_y = (y - radius), (y + radius)
for i in range(start_y, end_y+1):
for j in range(start_x, end_x+1):
if i >= 0 and i < self.len and j >= 0 and j < self.len:
if board[i][j] != 0:
return True
return False
# get all positions near chess
def genmove(self, board, turn):
fives = []
mfours, ofours = [], []
msfours, osfours = [], []
if turn == MAP_ENTRY_TYPE.MAP_PLAYER_ONE:
mine = 1
opponent = 2
else:
mine = 2
opponent = 1
moves = []
radius = 1
for y in range(self.len):
for x in range(self.len):
if board[y][x] == 0 and self.hasNeighbor(board, x, y, radius):
score = self.pos_score[y][x]
moves.append((score, x, y))
moves.sort(reverse=True)
return moves
def __search(self, board, turn, depth, alpha = SCORE_MIN, beta = SCORE_MAX):
score = self.evaluate(board, turn)
if depth <= 0 or abs(score) >= SCORE_FIVE:
return score
moves = self.genmove(board, turn)
bestmove = None
self.alpha += len(moves)
# if there are no moves, just return the score
if len(moves) == 0:
return score
for _, x, y in moves:
board[y][x] = turn
if turn == MAP_ENTRY_TYPE.MAP_PLAYER_ONE:
op_turn = MAP_ENTRY_TYPE.MAP_PLAYER_TWO
else:
op_turn = MAP_ENTRY_TYPE.MAP_PLAYER_ONE
score = - self.__search(board, op_turn, depth - 1, -beta, -alpha)
board[y][x] = 0
self.belta += 1
# alpha/beta pruning
if score > alpha:
alpha = score
bestmove = (x, y)
if alpha >= beta:
break
if depth == self.maxdepth and bestmove:
self.bestmove = bestmove
return alpha
def search(self, board, turn, depth = 4):
self.maxdepth = depth
self.bestmove = None
score = self.__search(board, turn, depth)
x, y = self.bestmove
return score, x, y
def findBestChess(self, board, turn):
time1 = time.time()
self.alpha = 0
self.belta = 0
score, x, y = self.search(board, turn, AI_SEARCH_DEPTH)
time2 = time.time()
print('time[%.2f] (%d, %d), score[%d] alpha[%d] belta[%d]' % ((time2-time1), x, y, score, self.alpha, self.belta))
return (x, y)
# calculate score, FIXME: May Be Improved
def getScore(self, mine_count, opponent_count):
mscore, oscore = 0, 0
if mine_count[FIVE] > 0:
return (SCORE_FIVE, 0)
if opponent_count[FIVE] > 0:
return (0, SCORE_FIVE)
if mine_count[SFOUR] >= 2:
mine_count[FOUR] += 1
if opponent_count[SFOUR] >= 2:
opponent_count[FOUR] += 1
if mine_count[FOUR] > 0:
return (9050, 0)
if mine_count[SFOUR] > 0:
return (9040, 0)
if opponent_count[FOUR] > 0:
return (0, 9030)
if opponent_count[SFOUR] > 0 and opponent_count[THREE] > 0:
return (0, 9020)
if mine_count[THREE] > 0 and opponent_count[SFOUR] == 0:
return (9010, 0)
if (opponent_count[THREE] > 1 and mine_count[THREE] == 0 and mine_count[STHREE] == 0):
return (0, 9000)
if opponent_count[SFOUR] > 0:
oscore += 400
if mine_count[THREE] > 1:
mscore += 500
elif mine_count[THREE] > 0:
mscore += 100
if opponent_count[THREE] > 1:
oscore += 2000
elif opponent_count[THREE] > 0:
oscore += 400
if mine_count[STHREE] > 0:
mscore += mine_count[STHREE] * 10
if opponent_count[STHREE] > 0:
oscore += opponent_count[STHREE] * 10
if mine_count[TWO] > 0:
mscore += mine_count[TWO] * 6
if opponent_count[TWO] > 0:
oscore += opponent_count[TWO] * 6
if mine_count[STWO] > 0:
mscore += mine_count[STWO] * 2
if opponent_count[STWO] > 0:
oscore += opponent_count[STWO] * 2
return (mscore, oscore)
def evaluate(self, board, turn, checkWin=False):
self.reset()
if turn == MAP_ENTRY_TYPE.MAP_PLAYER_ONE:
mine = 1
opponent = 2
else:
mine = 2
opponent = 1
for y in range(self.len):
for x in range(self.len):
if board[y][x] == mine:
self.evaluatePoint(board, x, y, mine, opponent)
elif board[y][x] == opponent:
self.evaluatePoint(board, x, y, opponent, mine)
mine_count = self.count[mine-1]
opponent_count = self.count[opponent-1]
if checkWin:
return mine_count[FIVE] > 0
else:
mscore, oscore = self.getScore(mine_count, opponent_count)
return (mscore - oscore)
def evaluatePoint(self, board, x, y, mine, opponent, count=None):
dir_offset = [(1, 0), (0, 1), (1, 1), (1, -1)] # direction from left to right
ignore_record = True
if count is None:
count = self.count[mine-1]
ignore_record = False
for i in range(4):
if self.record[y][x][i] == 0 or ignore_record:
self.analysisLine(board, x, y, i, dir_offset[i], mine, opponent, count)
# line is fixed len 9: XXXXMXXXX
def getLine(self, board, x, y, dir_offset, mine, opponent):
line = [0 for i in range(9)]
tmp_x = x + (-5 * dir_offset[0])
tmp_y = y + (-5 * dir_offset[1])
for i in range(9):
tmp_x += dir_offset[0]
tmp_y += dir_offset[1]
if (tmp_x < 0 or tmp_x >= self.len or
tmp_y < 0 or tmp_y >= self.len):
line[i] = opponent # set out of range as opponent chess
else:
line[i] = board[tmp_y][tmp_x]
return line
def analysisLine(self, board, x, y, dir_index, dir, mine, opponent, count):
# record line range[left, right] as analysized
def setRecord(self, x, y, left, right, dir_index, dir_offset):
tmp_x = x + (-5 + left) * dir_offset[0]
tmp_y = y + (-5 + left) * dir_offset[1]
for i in range(left, right+1):
tmp_x += dir_offset[0]
tmp_y += dir_offset[1]
self.record[tmp_y][tmp_x][dir_index] = 1
empty = MAP_ENTRY_TYPE.MAP_EMPTY.value
left_idx, right_idx = 4, 4
line = self.getLine(board, x, y, dir, mine, opponent)
while right_idx < 8:
if line[right_idx+1] != mine:
break
right_idx += 1
while left_idx > 0:
if line[left_idx-1] != mine:
break
left_idx -= 1
left_range, right_range = left_idx, right_idx
while right_range < 8:
if line[right_range+1] == opponent:
break
right_range += 1
while left_range > 0:
if line[left_range-1] == opponent:
break
left_range -= 1
chess_range = right_range - left_range + 1
if chess_range < 5:
setRecord(self, x, y, left_range, right_range, dir_index, dir)
return CHESS_TYPE.NONE
setRecord(self, x, y, left_idx, right_idx, dir_index, dir)
m_range = right_idx - left_idx + 1
# M:mine chess, P:opponent chess or out of range, X: empty
if m_range >= 5:
count[FIVE] += 1
# Live Four : XMMMMX
# Chong Four : XMMMMP, PMMMMX
if m_range == 4:
left_empty = right_empty = False
if line[left_idx-1] == empty:
left_empty = True
if line[right_idx+1] == empty:
right_empty = True
if left_empty and right_empty:
count[FOUR] += 1
elif left_empty or right_empty:
count[SFOUR] += 1
# Chong Four : MXMMM, MMMXM, the two types can both exist
# Live Three : XMMMXX, XXMMMX
# Sleep Three : PMMMX, XMMMP, PXMMMXP
if m_range == 3:
left_empty = right_empty = False
left_four = right_four = False
if line[left_idx-1] == empty:
if line[left_idx-2] == mine: # MXMMM
setRecord(self, x, y, left_idx-2, left_idx-1, dir_index, dir)
count[SFOUR] += 1
left_four = True
left_empty = True
if line[right_idx+1] == empty:
if line[right_idx+2] == mine: # MMMXM
setRecord(self, x, y, right_idx+1, right_idx+2, dir_index, dir)
count[SFOUR] += 1
right_four = True
right_empty = True
if left_four or right_four:
pass
elif left_empty and right_empty:
if chess_range > 5: # XMMMXX, XXMMMX
count[THREE] += 1
else: # PXMMMXP
count[STHREE] += 1
elif left_empty or right_empty: # PMMMX, XMMMP
count[STHREE] += 1
# Chong Four: MMXMM, only check right direction
# Live Three: XMXMMX, XMMXMX the two types can both exist
# Sleep Three: PMXMMX, XMXMMP, PMMXMX, XMMXMP
# Live Two: XMMX
# Sleep Two: PMMX, XMMP
if m_range == 2:
left_empty = right_empty = False
left_three = right_three = False
if line[left_idx-1] == empty:
if line[left_idx-2] == mine:
setRecord(self, x, y, left_idx-2, left_idx-1, dir_index, dir)
if line[left_idx-3] == empty:
if line[right_idx+1] == empty: # XMXMMX
count[THREE] += 1
else: # XMXMMP
count[STHREE] += 1
left_three = True
elif line[left_idx-3] == opponent: # PMXMMX
if line[right_idx+1] == empty:
count[STHREE] += 1
left_three = True
left_empty = True
if line[right_idx+1] == empty:
if line[right_idx+2] == mine:
if line[right_idx+3] == mine: # MMXMM
setRecord(self, x, y, right_idx+1, right_idx+2, dir_index, dir)
count[SFOUR] += 1
right_three = True
elif line[right_idx+3] == empty:
#setRecord(self, x, y, right_idx+1, right_idx+2, dir_index, dir)
if left_empty: # XMMXMX
count[THREE] += 1
else: # PMMXMX
count[STHREE] += 1
right_three = True
elif left_empty: # XMMXMP
count[STHREE] += 1
right_three = True
right_empty = True
if left_three or right_three:
pass
elif left_empty and right_empty: # XMMX
count[TWO] += 1
elif left_empty or right_empty: # PMMX, XMMP
count[STWO] += 1
# Live Two: XMXMX, XMXXMX only check right direction
# Sleep Two: PMXMX, XMXMP
if m_range == 1:
left_empty = right_empty = False
if line[left_idx-1] == empty:
if line[left_idx-2] == mine:
if line[left_idx-3] == empty:
if line[right_idx+1] == opponent: # XMXMP
count[STWO] += 1
left_empty = True
if line[right_idx+1] == empty:
if line[right_idx+2] == mine:
if line[right_idx+3] == empty:
if left_empty: # XMXMX
#setRecord(self, x, y, left_idx, right_idx+2, dir_index, dir)
count[TWO] += 1
else: # PMXMX
count[STWO] += 1
elif line[right_idx+2] == empty:
if line[right_idx+3] == mine and line[right_idx+4] == empty: # XMXXMX
count[TWO] += 1
return CHESS_TYPE.NONE