Spaces:
Sleeping
Sleeping
from enum import Enum | |
import math | |
from .shape import shapes, get_shape_fast, is_five, is_four, get_all_shapes_of_point | |
from .position import coordinate2Position, isLine, isAllInLine, hasInLine, position2Coordinate | |
from .config import config | |
from datetime import datetime | |
import os | |
dir_path = os.path.dirname(os.path.abspath(__file__)) | |
import numpy as np | |
import torch | |
# from .minimax_Net import BoardEvaluationNet as net | |
# mini_max_net = net(board_size=15) | |
# mini_max_net.load_state_dict(torch.load(os.path.join(dir_path, 'train_data/model', 'best_loss=609.3356355479785.pth'))) | |
# mini_max_net.eval() | |
# Enum to represent different shapes | |
class Shapes(Enum): | |
FIVE = 0 | |
BLOCK_FIVE = 1 | |
FOUR = 2 | |
FOUR_FOUR = 3 | |
FOUR_THREE = 4 | |
THREE_THREE = 5 | |
BLOCK_FOUR = 6 | |
THREE = 7 | |
BLOCK_THREE = 8 | |
TWO_TWO = 9 | |
TWO = 10 | |
NONE = 11 | |
# Constants representing scores for each shape | |
FIVE = 10000000 | |
BLOCK_FIVE = FIVE | |
FOUR = 100000 | |
FOUR_FOUR = FOUR # 双冲四 | |
FOUR_THREE = FOUR # 冲四活三 | |
THREE_THREE = FOUR / 2 # 双活三 | |
BLOCK_FOUR = 1500 | |
THREE = 1000 | |
BLOCK_THREE = 150 | |
TWO_TWO = 200 # 双活二 | |
TWO = 100 | |
BLOCK_TWO = 15 | |
ONE = 10 | |
BLOCK_ONE = 1 | |
# Function to calculate the real shape score based on the shape | |
def getRealShapeScore(shape: Shapes) -> int: | |
# Checked | |
if shape == shapes['FIVE']: | |
return FOUR | |
elif shape == shapes['BLOCK_FIVE']: | |
return BLOCK_FOUR | |
elif shape in [shapes['FOUR'], shapes['FOUR_FOUR'], shapes['FOUR_THREE']]: | |
return THREE | |
elif shape == shapes['BLOCK_FOUR']: | |
return BLOCK_THREE | |
elif shape == shapes['THREE']: | |
return TWO | |
elif shape == shapes['THREE_THREE']: | |
return math.floor(THREE_THREE / 10) | |
elif shape == shapes['BLOCK_THREE']: | |
return BLOCK_TWO | |
elif shape == shapes['TWO']: | |
return ONE | |
elif shape == shapes['TWO_TWO']: | |
return math.floor(TWO_TWO / 10) | |
else: | |
return 0 | |
# List of all directions | |
allDirections = [ | |
[0, 1], # Horizontal | |
[1, 0], # Vertical | |
[1, 1], # Diagonal \ | |
[1, -1] # Diagonal / | |
] | |
# Function to get the index of a direction | |
def direction2index(ox: int, oy: int) -> int: | |
# Checked | |
if ox == 0: | |
return 0 # | | |
elif oy == 0: | |
return 1 # - | |
elif ox == oy: | |
return 2 # \ | |
elif ox != oy: | |
return 3 # / | |
# Performance dictionary | |
performance = { | |
"updateTime": 0, | |
"getPointsTime": 0 | |
} | |
class Evaluate: | |
def __init__(self, size=15): | |
# Checked | |
self.size = size | |
self.board = [[2] * (size + 2) for _ in range(size + 2)] | |
for i in range(size + 2): | |
for j in range(size + 2): | |
if i == 0 or j == 0 or i == self.size + 1 or j == self.size + 1: | |
self.board[i][j] = 2 | |
else: | |
self.board[i][j] = 0 | |
self.blackScores = [[0] * self.size for _ in range(size)] | |
self.whiteScores = [[0] * self.size for _ in range(size)] | |
self.initPoints() | |
self.history = [] # List of [position, role] | |
def move(self, x, y, role): | |
# Checked | |
# Clear the cache first | |
for d in [0, 1, 2, 3]: | |
self.shapeCache[role][d][x][y] = 0 | |
self.shapeCache[-role][d][x][y] = 0 | |
self.blackScores[x][y] = 0 | |
self.whiteScores[x][y] = 0 | |
# Update the board | |
self.board[x + 1][y + 1] = role ## Adjust for the added wall | |
self.updatePoint(x, y) | |
self.history.append([coordinate2Position(x, y, self.size), role]) | |
def undo(self, x, y): | |
# Checked | |
self.board[x + 1][y + 1] = 0 | |
self.updatePoint(x, y) | |
self.history.pop() | |
def initPoints(self): | |
# Checked | |
# Initialize the cache, avoid calculating the same points multiple times | |
self.shapeCache = {} | |
for role in [1, -1]: | |
self.shapeCache[role] = {} | |
for direction in [0, 1, 2, 3]: | |
self.shapeCache[role][direction] = [[0] * self.size for _ in range(self.size)] | |
self.pointsCache = {} | |
for role in [1, -1]: | |
self.pointsCache[role] = {} | |
for shape in shapes: | |
self.pointsCache[role][shape] = set() | |
def getPointsInLine(self, role): | |
# Checked | |
pointsInLine = {} | |
hasPointsInLine = False | |
for key in shapes: | |
pointsInLine[shapes[key]] = set() | |
last2Points = [position for position, role in self.history[-config['inlineCount']:]] | |
processed = {} | |
# 在last2Points中查找是否有点位在一条线上 | |
for r in [role, -role]: | |
for point in last2Points: | |
x, y = position2Coordinate(point, self.size) | |
for ox, oy in allDirections: | |
for sign in [1, -1]: | |
for step in range(1, config['inLineDistance'] + 1): | |
nx = x + sign * step * ox | |
ny = y + sign * step * oy | |
position = coordinate2Position(nx, ny, self.size) | |
# 检测是否到达边界 | |
if nx < 0 or nx >= self.size or ny < 0 or ny >= self.size: | |
break | |
if self.board[nx + 1][ny + 1] != 0: | |
continue | |
if processed.get(position) == r: | |
continue | |
processed[position] = r | |
for direction in [0, 1, 2, 3]: | |
shape = self.shapeCache[r][direction][nx][ny] | |
# 到达边界停止,但是注意到达对方棋子不能停止 | |
if shape: | |
pointsInLine[shape].add(coordinate2Position(nx, ny, self.size)) | |
hasPointsInLine = True | |
if hasPointsInLine: | |
return pointsInLine | |
return False | |
def getPoints(self, role, depth, vct, vcf): | |
first = role if depth % 2 == 0 else -role # 先手 | |
start = datetime.now() | |
if config['onlyInLine'] and len(self.history) >= config['inlineCount']: | |
points_in_line = self.getPointsInLine(role) | |
if points_in_line: | |
performance['getPointsTime'] += (datetime.now() - start).total_seconds() | |
return points_in_line | |
points = {} # 全部点位 | |
for key in shapes.keys(): | |
points[shapes[key]] = set() | |
last_points = [position for position, _ in self.history[-4:]] | |
for r in [role, -role]: | |
# 这里是直接遍历了这个棋盘上的所有点位,如果棋盘很大,这里会有性能问题;可以用神经网络来预测 | |
for i in range(self.size): | |
for j in range(self.size): | |
four_count = 0 | |
block_four_count = 0 | |
three_count = 0 | |
for direction in [0, 1, 2, 3]: | |
if self.board[i + 1][j + 1] != 0: | |
continue | |
shape = self.shapeCache[r][direction][i][j] | |
if not shape: | |
continue | |
point = i * self.size + j | |
if vcf: | |
if r == first and not is_four(shape) and not is_five(shape): | |
continue | |
if r == -first and is_five(shape): | |
continue | |
if vct: | |
if depth % 2 == 0: | |
if depth == 0 and r != first: | |
continue | |
if shape != shapes['THREE'] and not is_four(shape) and not is_five(shape): | |
continue | |
if shape == shapes['THREE'] and r != first: | |
continue | |
if depth == 0 and r != first: | |
continue | |
if depth > 0: | |
if shape == shapes['THREE'] and len( | |
get_all_shapes_of_point(self.shapeCache, i, j, r)) == 1: | |
continue | |
if shape == shapes['BLOCK_FOUR'] and len( | |
get_all_shapes_of_point(self.shapeCache, i, j, r)) == 1: | |
continue | |
else: | |
if shape != shapes['THREE'] and not is_four(shape) and not is_five(shape): | |
continue | |
if shape == shapes['THREE'] and r == -first: | |
continue | |
if depth > 1: | |
if shape == shapes['BLOCK_FOUR'] and len( | |
get_all_shapes_of_point(self.shapeCache, i, j)) == 1: | |
continue | |
if shape == shapes['BLOCK_FOUR'] and not hasInLine(point, last_points, self.size): | |
continue | |
if vcf: | |
if not is_four(shape) and not is_five(shape): | |
continue | |
if depth > 2 and (shape == shapes['TWO'] or shape == shapes['TWO_TWO'] or shape == shapes[ | |
'BLOCK_THREE']) and not hasInLine(point, last_points, self.size): | |
continue | |
points[shape].add(point) | |
if shape == shapes['FOUR']: | |
four_count += 1 | |
elif shape == shapes['BLOCK_FOUR']: | |
block_four_count += 1 | |
elif shape == shapes['THREE']: | |
three_count += 1 | |
union_shape = None | |
if four_count >= 2: | |
union_shape = shapes['FOUR_FOUR'] | |
elif block_four_count and three_count: | |
union_shape = shapes['FOUR_THREE'] | |
elif three_count >= 2: | |
union_shape = shapes['THREE_THREE'] | |
if union_shape: | |
points[union_shape].add(point) | |
performance['getPointsTime'] += (datetime.now() - start).total_seconds() | |
return points | |
""" | |
当一个位置发生变时候,要更新这个位置的四个方向上得分,更新规则是: | |
1. 如果这个位置是空的,那么就重新计算这个位置的得分 | |
2. 如果碰到了边界或者对方的棋子,那么就停止计算 | |
3. 如果超过2个空位,那么就停止计算 | |
4. 要更新自己的和对方的得分 | |
""" | |
def updatePoint(self, x, y): | |
# Checked | |
start = datetime.now() | |
self.updateSinglePoint(x, y, 1) | |
self.updateSinglePoint(x, y, -1) | |
for ox, oy in allDirections: | |
for sign in [1, -1]: # -1 for negative direction, 1 for positive direction | |
for step in range(1, 6): | |
reachEdge = False | |
for role in [1, -1]: | |
nx = x + sign * step * ox + 1 # +1 to adjust for wall | |
ny = y + sign * step * oy + 1 # +1 to adjust for wall | |
# Stop if wall or opponent's piece is found | |
if self.board[nx][ny] == 2: | |
reachEdge = True | |
break | |
elif self.board[nx][ny] == -role: # Change role if opponent's piece is found | |
continue | |
elif self.board[nx][ny] == 0: | |
self.updateSinglePoint(nx - 1, ny - 1, role, | |
[sign * ox, sign * oy]) # -1 to adjust back from wall | |
if reachEdge: | |
break | |
performance['updateTime'] += (datetime.now() - start).total_seconds() | |
""" | |
计算单个点的得分 | |
计算原理是: | |
在当前位置放一个当前角色的棋子,遍历四个方向,生成四个方向上的字符串,用patters来匹配字符串, 匹配到的话,就将对应的得分加到scores上 | |
四个方向的字符串生成规则是:向两边都延伸5个位置,如果遇到边界或者对方的棋子,就停止延伸 | |
在更新周围棋子时,只有一个方向需要更新,因此可以传入direction参数,只更新一个方向 | |
""" | |
def updateSinglePoint(self, x, y, role, direction=None): | |
# Checked | |
if self.board[x + 1][y + 1] != 0: | |
return # Not an empty spot | |
# Temporarily place the piece | |
self.board[x + 1][y + 1] = role | |
directions = [] | |
if direction: | |
directions.append(direction) | |
else: | |
directions = allDirections | |
shapeCache = self.shapeCache[role] | |
# Clear the cache first | |
for ox, oy in directions: | |
shapeCache[direction2index(ox, oy)][x][y] = shapes['NONE'] | |
score = 0 | |
blockFourCount = 0 | |
threeCount = 0 | |
twoCount = 0 | |
# Calculate existing score | |
for intDirection in [0, 1, 2, 3]: | |
shape = shapeCache[intDirection][x][y] | |
if shape > shapes['NONE']: | |
score += getRealShapeScore(shape) | |
if shape == shapes['BLOCK_FOUR']: | |
blockFourCount += 1 | |
if shape == shapes['THREE']: | |
threeCount += 1 | |
if shape == shapes['TWO']: | |
twoCount += 1 | |
for ox, oy in directions: | |
intDirection = direction2index(ox, oy) | |
shape, selfCount = get_shape_fast(self.board, x, y, ox, oy, role) | |
if not shape: | |
continue | |
if shape: | |
# Note: Only cache single shapes, do not cache compound shapes like double threes, as they depend on two shapes | |
shapeCache[intDirection][x][y] = shape | |
if shape == shapes['BLOCK_FOUR']: | |
blockFourCount += 1 | |
if shape == shapes['THREE']: | |
threeCount += 1 | |
if shape == shapes['TWO']: | |
twoCount += 1 | |
if blockFourCount >= 2: | |
shape = shapes['FOUR_FOUR'] | |
elif blockFourCount and threeCount: | |
shape = shapes['FOUR_THREE'] | |
elif threeCount >= 2: | |
shape = shapes['THREE_THREE'] | |
elif twoCount >= 2: | |
shape = shapes['TWO_TWO'] | |
score += getRealShapeScore(shape) | |
self.board[x + 1][y + 1] = 0 # Remove the temporary piece | |
if role == 1: | |
self.blackScores[x][y] = score | |
else: | |
self.whiteScores[x][y] = score | |
return score | |
def evaluate(self, role): | |
# Checked | |
blackScore = 0 | |
whiteScore = 0 | |
for i in range(len(self.blackScores)): | |
for j in range(len(self.blackScores[i])): | |
blackScore += self.blackScores[i][j] | |
for i in range(len(self.whiteScores)): | |
for j in range(len(self.whiteScores[i])): | |
whiteScore += self.whiteScores[i][j] | |
score = blackScore - whiteScore if role == 1 else whiteScore - blackScore | |
return score | |
def getMoves(self, role, depth, onThree=False, onlyFour=False, use_net = False): | |
# Checked | |
train_data = 0 | |
if use_net and role == 1: | |
# value_move_num = 6 | |
# input = torch.Tensor(np.array(self.board)[1:-1, 1:-1]).unsqueeze(0) | |
# scores = mini_max_net(input) | |
# flattened_scores = scores.flatten() | |
# | |
# moves = (flattened_scores.argsort(descending=True)[:value_move_num]).tolist() | |
moves = 0 | |
# print(moves) | |
else: | |
moves, model_train_maxtrix = self._getMoves(role, depth, onThree, onlyFour) | |
train_data = {"state": np.array(self.board)[1:-1, 1:-1], "scores": model_train_maxtrix} | |
moves = [(move // self.size, move % self.size) for move in moves] | |
# cut the self.board into normal size | |
print("moves", moves) | |
return moves, train_data | |
def _getMoves(self, role, depth, only_three=False, only_four=False): | |
""" | |
Get possible moves based on the current game state. | |
""" | |
points = self.getPoints(role, depth, only_three, only_four) | |
fives = points[shapes['FIVE']] | |
block_fives = points[shapes['BLOCK_FIVE']] | |
# To train the model, we need to get all these points's score and store it to board size matrix | |
# Then we can use this matrix to train the model, given a state, we want it to output the score of each point, then we can choose the highest score point | |
model_train_matrix = [[0] * self.size for _ in range(self.size)] | |
if fives and len(fives) > 0 or block_fives and len(block_fives) > 0: | |
for point in fives: | |
x = point // self.size | |
y = point % self.size | |
for point in block_fives: | |
x = point // self.size | |
y = point % self.size | |
return set(list(fives) + list(block_fives)), model_train_matrix | |
fours = points[shapes['FOUR']] | |
block_fours = points[shapes['BLOCK_FOUR']] # Block four is special, consider it in both four and three | |
if only_four or (fours and len(fours) > 0): | |
for point in fours: | |
x = point // self.size | |
y = point % self.size | |
for point in block_fours: | |
x = point // self.size | |
y = point % self.size | |
return set(list(fours) + list(block_fours)), model_train_matrix | |
four_fours = points[shapes['FOUR_FOUR']] | |
if four_fours and len(four_fours) > 0: | |
for point in four_fours: | |
x = point // self.size | |
y = point % self.size | |
for point in block_fours: | |
x = point // self.size | |
y = point % self.size | |
return set(list(four_fours) + list(block_fours)), model_train_matrix | |
# Double threes and active threes | |
threes = points[shapes['THREE']] | |
four_threes = points[shapes['FOUR_THREE']] | |
if four_threes and len(four_threes) > 0: | |
for point in four_threes: | |
x = point // self.size | |
y = point % self.size | |
for point in block_fours: | |
x = point // self.size | |
y = point % self.size | |
for point in threes: | |
x = point // self.size | |
y = point % self.size | |
return set(list(four_threes) + list(block_fours) + list(threes)), model_train_matrix | |
three_threes = points[shapes['THREE_THREE']] | |
if three_threes and len(three_threes) > 0: | |
for point in three_threes: | |
x = point // self.size | |
y = point % self.size | |
for point in block_fours: | |
x = point // self.size | |
y = point % self.size | |
for point in threes: | |
x = point // self.size | |
y = point % self.size | |
return set(list(three_threes) + list(block_fours) + list(threes)), model_train_matrix | |
if only_three: | |
for point in threes: | |
x = point // self.size | |
y = point % self.size | |
for point in block_fours: | |
x = point // self.size | |
y = point % self.size | |
return set(list(block_fours) + list(threes)), model_train_matrix | |
block_threes = points[shapes['BLOCK_THREE']] | |
two_twos = points[shapes['TWO_TWO']] | |
twos = points[shapes['TWO']] | |
mid = list(block_fours) + list(threes) + list(block_threes) + list(two_twos) + list(twos) | |
res = set(mid[:5]) | |
for i in range(len(model_train_matrix)): | |
for j in range(len(model_train_matrix)): | |
if (i * len(model_train_matrix) + j) not in res: | |
model_train_matrix[i][j] = 0 | |
return res, model_train_matrix | |