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 max_min
極大極小值搜索是一個深度優先的算法,當第二層第一個節點的子節點都計算好評分後,因爲這層是MIN層,會選取子節點中最低的評分作爲這個節點的評分,就是3。依次類推,第二層第二個節點評分爲6,第三個節點爲5。當第二層節點都獲取到評分後,因爲第一層是MAX層,會選取子節點中最高的評分作爲這個節點的評分,就是第二層第二個節點的評分6,這個節點所代表的下棋位置對於AI來說就是最有利的。最後算法返回評分6和第二層第二個節點的下棋位置。

根節點表示當前的棋局,博弈樹上每一個節點就是從樹根節點開始,每個子節點下一步棋,到該節點形成的新的棋局。 棋局就是所有已下棋位置的順序列表,比如[ (7,7), (8,8), (7,9) ]。
圖2 tree
比如圖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 1

如圖4,是搜索完節點B,並更新了節點B和根節點A的α值後,開始搜索第二層第二個節點C時的情況。根節點A的α值更新爲當前最有利的評分3。節點C的α, β值設爲(+∞, 3),β值是父節點A當前的α值。
圖4 2

如圖5,是搜索完節點C,並更新了節點C和根節點A的α值後,開始搜索第二層第三個節點D時的情況。節點C的α值更新爲6,根節點A的α值更新爲當前最有利的評分6。節點D的α, β值設爲(+∞, 6),β值是父節點A當前的α值。
圖5 3

如圖6,就是MIN層剪枝的過程,搜索節點D的第一個子節點,得到的評分是5,更新節點D的α值爲5,這時節點D 符合MIN層的剪枝判斷: α值 ≤ β值,所以節點D的第二個子節點就被裁剪了。
圖6 4

MAX層剪枝

再看一個MAX層剪枝的例子,根節點A的α, β值爲(-∞, +∞),博弈樹層數爲4。
如圖7,是搜索完第三層第一個節點C的第一個子節點的情況。節點B是在MIN層,所以α, β值設爲(+∞, -∞),節點C是在MAX層,所以α, β值設爲(-∞,+∞)。節點C的第一個子節點初始α, β值設爲(+∞, -∞),搜索完後α值更新爲5。
圖7 5

如圖8,是搜索完節點C後,更新完節點B的情況,節點C的α值更新爲5, 節點B的α值同樣更新爲5。
圖8 6

如圖9,是開始搜索節點B的第二個子節點D時的情況。節點D在MAX層,所以α, β值設爲(-∞, 5), β值是父節點B當前的α值。
圖9 7

如圖10,是搜索完節點D的第一個子節點後的情況。節點D的第一個子節點的評分爲7,更新節點D的α值爲7,這時節點D 符合MAX層的剪枝判斷: α值 >= β值,所以節點D的第二個子節點就被裁剪了。
圖10 8

代碼實現

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.pyChessAI.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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章