人工智能系列--nullmove

本文详细探讨了nullmove剪枝策略在棋类游戏AI中的应用,包括其实现细节及限制条件,如攻击子数量限制、搜索深度要求等,并讨论了如何验证nullmove的有效性。
部署运行你感兴趣的模型镜像
 
#1  Nullmove 实战剖析

int attackpieces = (side==RED?(Rattackpieces) : (Battackpieces));

  int nulldepth = CtrlNullEx ? 4 : 3;

if (CtrlNullmove
        && !NullVerify
        && !InChk[ply]
        && !mate_threat
        && attackpieces        > 0
        && !avoid_donull
        && depth>=2
        && (depth-nulldepth<=0 || zEval(side, ply, 0)>=beta)
        && do_null)
{ // 控制nullmove的危险程度

        null_score = -zSearch(-beta, -beta + 1, 1 - side, depth - nulldepth, ply + 1, false/*do_null = false*/);

        if (null_score >= beta)
        {
                {
                        bool verifyok = false;

                        // null-move verify
                        if (depth < nulldepth)
                                verifyok = true;
                        else
                        {
                                NullVerify = true;

                                if (zSearch(beta-1, beta, side, depth-nulldepth+1, ply, false) >= beta)
                                        verifyok = true;

                                NullVerify = false;

                                ///*
                                if (verifyok) {
                                        StoreHash();
                                }
                                //*/
                        }

                        if (verifyok)
                                return (null_score);
                }
        }

        else if (null_score == -vMate + ply + nulldepth)
                mate_threat = 1;
}
nullmove是一种有损剪枝, 会带来一些不准确的结果, 所以控制nullmove的危险程度是很有必要的

在何种条件下可以进行nullmove? 我的实现算法中, 采取了以下几种限制
1. 攻击子>0
2. depth>=2 最后一层直接剪掉是非常危险的
3. 不被将军
4. 上一层已经做了nullmove, 本层就不再做了
5. 当校验nullmove是否正确时,不再使用nullmove
6. 当同样的盘面,以前曾经使用过nullmove,并且发现被对方杀死时(参考crafty)
7. 层数比nulldepth要大, 或者优势很大时 (参考fruit)
8. avoid_donull发生在如果保存在hash表中有记录,但是却不能返回结果时

其中7是相当好的一种限制条件, 比通过子力的多少来进行限制有效得多

因为nullmove的危险性, 需要测试nullmove结果是否正确, 通过统计, 子力多的时候, 基本不会出现verify失败的结果, 这多少也印证了crafty中, 采用子力多少进行nullmove限制的原因

nullmove的verify是通过层数多一层的搜索来校验的方法来校验, 所以
depth小于nulldepth时,可以认为不需要校验
(考虑这个时候计算的结果跟nullmove搜索结果的比较)

nullmove搜索的结果可以保存重用(参考crafty)

这里对象眼nullmove的实现, 提几点建议

                if (Search.bNullMove
                        && !bNoNull
                        && !mvsLast.bcCheck
                        && Tree.pos.NullMoveOkay()
                        && nDepth>1
                        && (nDepth-NULL_DEPTH-1<=0 || Tree.pos.Evaluate(vlAlpha, vlBeta)>=vlBeta)
                        )
                {
                        Tree.pos.MakeMove(0);
                        vl = -SearchFull(Tree, -vlBeta, 1 - vlBeta, nDepth - NULL_DEPTH - 1, NO_NULL);
                        Tree.pos.UndoMakeMove();
                        if (vl >= vlBeta)
                        {
                                if (Tree.pos.NullMoveSafe() || nDepth<=NULL_DEPTH || SearchFull(Tree, vlBeta - 1, vlBeta, nDepth - NULL_DEPTH, NO_NULL) >= vlBeta)
                                {
                                        return FAIL_SOFT ? vl : vlBeta;
                                }
                        }
                        else if (vl == nThisPly + 2 - MATE_VALUE)
                        {
                                bMateThreat = TRUE;
                        }
                }

其中的关键是(nDepth-NULL_DEPTH-1<=0 || Tree.pos.Evaluate(vlAlpha, vlBeta)>=vlBeta), 这样可以避免很多危险的情况, 使用了nullmove启发剪枝

您可能感兴趣的与本文相关的镜像

Stable-Diffusion-3.5

Stable-Diffusion-3.5

图片生成
Stable-Diffusion

Stable Diffusion 3.5 (SD 3.5) 是由 Stability AI 推出的新一代文本到图像生成模型,相比 3.0 版本,它提升了图像质量、运行速度和硬件效率

#!/usr/bin/env pypy3 from __future__ import print_function import time, math from itertools import count from collections import namedtuple, defaultdict # If we could rely on the env -S argument, we could just use "pypy3 -u" # as the shebang to unbuffer stdout. But alas we have to do this instead: #from functools import partial #print = partial(print, flush=True) version = "sunfish 2023" ############################################################################### # Piece-Square tables. Tune these to change sunfish's behaviour ############################################################################### # With xz compression this whole section takes 652 bytes. # That's pretty good given we have 64*6 = 384 values. # Though probably we could do better... # For one thing, they could easily all fit into int8. piece = {"P": 100, "N": 280, "B": 320, "R": 479, "Q": 929, "K": 60000} pst = { 'P': ( 0, 0, 0, 0, 0, 0, 0, 0, 78, 83, 86, 73, 102, 82, 85, 90, 7, 29, 21, 44, 40, 31, 44, 7, -17, 16, -2, 15, 14, 0, 15, -13, -26, 3, 10, 9, 6, 1, 0, -23, -22, 9, 5, -11, -10, -2, 3, -19, -31, 8, -7, -37, -36, -14, 3, -31, 0, 0, 0, 0, 0, 0, 0, 0), 'N': ( -66, -53, -75, -75, -10, -55, -58, -70, -3, -6, 100, -36, 4, 62, -4, -14, 10, 67, 1, 74, 73, 27, 62, -2, 24, 24, 45, 37, 33, 41, 25, 17, -1, 5, 31, 21, 22, 35, 2, 0, -18, 10, 13, 22, 18, 15, 11, -14, -23, -15, 2, 0, 2, 0, -23, -20, -74, -23, -26, -24, -19, -35, -22, -69), 'B': ( -59, -78, -82, -76, -23,-107, -37, -50, -11, 20, 35, -42, -39, 31, 2, -22, -9, 39, -32, 41, 52, -10, 28, -14, 25, 17, 20, 34, 26, 25, 15, 10, 13, 10, 17, 23, 17, 16, 0, 7, 14, 25, 24, 15, 8, 25, 20, 15, 19, 20, 11, 6, 7, 6, 20, 16, -7, 2, -15, -12, -14, -15, -10, -10), 'R': ( 35, 29, 33, 4, 37, 33, 56, 50, 55, 29, 56, 67, 55, 62, 34, 60, 19, 35, 28, 33, 45, 27, 25, 15, 0, 5, 16, 13, 18, -4, -9, -6, -28, -35, -16, -21, -13, -29, -46, -30, -42, -28, -42, -25, -25, -35, -26, -46, -53, -38, -31, -26, -29, -43, -44, -53, -30, -24, -18, 5, -2, -18, -31, -32), 'Q': ( 6, 1, -8,-104, 69, 24, 88, 26, 14, 32, 60, -10, 20, 76, 57, 24, -2, 43, 32, 60, 72, 63, 43, 2, 1, -16, 22, 17, 25, 20, -13, -6, -14, -15, -2, -5, -1, -10, -20, -22, -30, -6, -13, -11, -16, -11, -16, -27, -36, -18, 0, -19, -15, -15, -21, -38, -39, -30, -31, -13, -31, -36, -34, -42), 'K': ( 4, 54, 47, -99, -99, 60, 83, -62, -32, 10, 55, 56, 56, 55, 10, 3, -62, 12, -57, 44, -67, 28, 37, -31, -55, 50, 11, -4, -19, 13, 0, -49, -55, -43, -52, -28, -51, -47, -8, -50, -47, -42, -43, -79, -64, -32, -29, -32, -4, 3, -14, -50, -57, -18, 13, 4, 17, 30, -3, -14, 6, -1, 40, 18), } # Pad tables and join piece and pst dictionaries for k, table in pst.items(): padrow = lambda row: (0,) + tuple(x + piece[k] for x in row) + (0,) pst[k] = sum((padrow(table[i * 8 : i * 8 + 8]) for i in range(8)), ()) pst[k] = (0,) * 20 + pst[k] + (0,) * 20 ############################################################################### # Global constants ############################################################################### # Our board is represented as a 120 character string. The padding allows for # fast detection of moves that don't stay within the board. A1, H1, A8, H8 = 91, 98, 21, 28 initial = ( " \n" # 0 - 9 " \n" # 10 - 19 " rnbqkbnr\n" # 20 - 29 " pppppppp\n" # 30 - 39 " ........\n" # 40 - 49 " ........\n" # 50 - 59 " ........\n" # 60 - 69 " ........\n" # 70 - 79 " PPPPPPPP\n" # 80 - 89 " RNBQKBNR\n" # 90 - 99 " \n" # 100 -109 " \n" # 110 -119 ) # Lists of possible moves for each piece type. N, E, S, W = -10, 1, 10, -1 directions = { "P": (N, N+N, N+W, N+E), "N": (N+N+E, E+N+E, E+S+E, S+S+E, S+S+W, W+S+W, W+N+W, N+N+W), "B": (N+E, S+E, S+W, N+W), "R": (N, E, S, W), "Q": (N, E, S, W, N+E, S+E, S+W, N+W), "K": (N, E, S, W, N+E, S+E, S+W, N+W) } # Mate value must be greater than 8*queen + 2*(rook+knight+bishop) # King value is set to twice this value such that if the opponent is # 8 queens up, but we got the king, we still exceed MATE_VALUE. # When a MATE is detected, we'll set the score to MATE_UPPER - plies to get there # E.g. Mate in 3 will be MATE_UPPER - 6 MATE_LOWER = piece["K"] - 10 * piece["Q"] MATE_UPPER = piece["K"] + 10 * piece["Q"] # Constants for tuning search QS = 40 QS_A = 140 EVAL_ROUGHNESS = 15 # minifier-hide start opt_ranges = dict( QS = (0, 300), QS_A = (0, 300), EVAL_ROUGHNESS = (0, 50), ) # minifier-hide end ############################################################################### # Chess logic ############################################################################### Move = namedtuple("Move", "i j prom") class Position(namedtuple("Position", "board score wc bc ep kp")): """A state of a chess game board -- a 120 char representation of the board score -- the board evaluation wc -- the castling rights, [west/queen side, east/king side] bc -- the opponent castling rights, [west/king side, east/queen side] ep - the en passant square kp - the king passant square """ def gen_moves(self): # For each of our pieces, iterate through each possible 'ray' of moves, # as defined in the 'directions' map. The rays are broken e.g. by # captures or immediately in case of pieces such as knights. for i, p in enumerate(self.board): if not p.isupper(): continue for d in directions[p]: for j in count(i + d, d): q = self.board[j] # Stay inside the board, and off friendly pieces if q.isspace() or q.isupper(): break # Pawn move, double move and capture if p == "P": if d in (N, N + N) and q != ".": break if d == N + N and (i < A1 + N or self.board[i + N] != "."): break if ( d in (N + W, N + E) and q == "." and j not in (self.ep, self.kp, self.kp - 1, self.kp + 1) #and j != self.ep and abs(j - self.kp) >= 2 ): break # If we move to the last row, we can be anything if A8 <= j <= H8: for prom in "NBRQ": yield Move(i, j, prom) break # Move it yield Move(i, j, "") # Stop crawlers from sliding, and sliding after captures if p in "PNK" or q.islower(): break # Castling, by sliding the rook next to the king if i == A1 and self.board[j + E] == "K" and self.wc[0]: yield Move(j + E, j + W, "") if i == H1 and self.board[j + W] == "K" and self.wc[1]: yield Move(j + W, j + E, "") def rotate(self, nullmove=False): """Rotates the board, preserving enpassant, unless nullmove""" return Position( self.board[::-1].swapcase(), -self.score, self.bc, self.wc, 119 - self.ep if self.ep and not nullmove else 0, 119 - self.kp if self.kp and not nullmove else 0, ) def move(self, move): i, j, prom = move p, q = self.board[i], self.board[j] put = lambda board, i, p: board[:i] + p + board[i + 1 :] # Copy variables and reset ep and kp board = self.board wc, bc, ep, kp = self.wc, self.bc, 0, 0 score = self.score + self.value(move) # Actual move board = put(board, j, board[i]) board = put(board, i, ".") # Castling rights, we move the rook or capture the opponent's if i == A1: wc = (False, wc[1]) if i == H1: wc = (wc[0], False) if j == A8: bc = (bc[0], False) if j == H8: bc = (False, bc[1]) # Castling if p == "K": wc = (False, False) if abs(j - i) == 2: kp = (i + j) // 2 board = put(board, A1 if j < i else H1, ".") board = put(board, kp, "R") # Pawn promotion, double move and en passant capture if p == "P": if A8 <= j <= H8: board = put(board, j, prom) if j - i == 2 * N: ep = i + N if j == self.ep: board = put(board, j + S, ".") # We rotate the returned position, so it's ready for the next player return Position(board, score, wc, bc, ep, kp).rotate() def value(self, move): i, j, prom = move p, q = self.board[i], self.board[j] # Actual move score = pst[p][j] - pst[p][i] # Capture if q.islower(): score += pst[q.upper()][119 - j] # Castling check detection if abs(j - self.kp) < 2: score += pst["K"][119 - j] # Castling if p == "K" and abs(i - j) == 2: score += pst["R"][(i + j) // 2] score -= pst["R"][A1 if j < i else H1] # Special pawn stuff if p == "P": if A8 <= j <= H8: score += pst[prom][j] - pst["P"][j] if j == self.ep: score += pst["P"][119 - (j + S)] return score ############################################################################### # Search logic ############################################################################### # lower <= s(pos) <= upper Entry = namedtuple("Entry", "lower upper") class Searcher: def __init__(self): self.tp_score = {} self.tp_move = {} self.history = set() self.nodes = 0 def bound(self, pos, gamma, depth, can_null=True): """ Let s* be the "true" score of the sub-tree we are searching. The method returns r, where if gamma > s* then s* <= r < gamma (A better upper bound) if gamma <= s* then gamma <= r <= s* (A better lower bound) """ self.nodes += 1 # Depth <= 0 is QSearch. Here any position is searched as deeply as is needed for # calmness, and from this point on there is no difference in behaviour depending on # depth, so so there is no reason to keep different depths in the transposition table. depth = max(depth, 0) # Sunfish is a king-capture engine, so we should always check if we # still have a king. Notice since this is the only termination check, # the remaining code has to be comfortable with being mated, stalemated # or able to capture the opponent king. if pos.score <= -MATE_LOWER: return -MATE_UPPER # Look in the table if we have already searched this position before. # We also need to be sure, that the stored search was over the same # nodes as the current search. entry = self.tp_score.get((pos, depth, can_null), Entry(-MATE_UPPER, MATE_UPPER)) if entry.lower >= gamma: return entry.lower if entry.upper < gamma: return entry.upper # Let's not repeat positions. We don't chat # - at the root (can_null=False) since it is in history, but not a draw. # - at depth=0, since it would be expensive and break "futility pruning". if can_null and depth > 0 and pos in self.history: return 0 # Generator of moves to search in order. # This allows us to define the moves, but only calculate them if needed. def moves(): # First try not moving at all. We only do this if there is at least one major # piece left on the board, since otherwise zugzwangs are too dangerous. # FIXME: We also can't null move if we can capture the opponent king. # Since if we do, we won't spot illegal moves that could lead to stalemate. # For now we just solve this by not using null-move in very unbalanced positions. # TODO: We could actually use null-move in QS as well. Not sure it would be very useful. # But still.... We just have to move stand-pat to be before null-move. #if depth > 2 and can_null and any(c in pos.board for c in "RBNQ"): #if depth > 2 and can_null and any(c in pos.board for c in "RBNQ") and abs(pos.score) < 500: if depth > 2 and can_null and abs(pos.score) < 500: yield None, -self.bound(pos.rotate(nullmove=True), 1 - gamma, depth - 3) # For QSearch we have a different kind of null-move, namely we can just stop # and not capture anything else. if depth == 0: yield None, pos.score # Look for the strongest ove from last time, the hash-move. killer = self.tp_move.get(pos) # If there isn't one, try to find one with a more shallow search. # This is known as Internal Iterative Deepening (IID). We set # can_null=True, since we want to make sure we actually find a move. if not killer and depth > 2: self.bound(pos, gamma, depth - 3, can_null=False) killer = self.tp_move.get(pos) # If depth == 0 we only try moves with high intrinsic score (captures and # promotions). Otherwise we do all moves. This is called quiescent search. val_lower = QS - depth * QS_A # Only play the move if it would be included at the current val-limit, # since otherwise we'd get search instability. # We will search it again in the main loop below, but the tp will fix # things for us. if killer and pos.value(killer) >= val_lower: yield killer, -self.bound(pos.move(killer), 1 - gamma, depth - 1) # Then all the other moves for val, move in sorted(((pos.value(m), m) for m in pos.gen_moves()), reverse=True): # Quiescent search if val < val_lower: break # If the new score is less than gamma, the opponent will for sure just # stand pat, since ""pos.score + val < gamma === -(pos.score + val) >= 1-gamma"" # This is known as futility pruning. if depth <= 1 and pos.score + val < gamma: # Need special case for MATE, since it would normally be caught # before standing pat. yield move, pos.score + val if val < MATE_LOWER else MATE_UPPER # We can also break, since we have ordered the moves by value, # so it can't get any better than this. break yield move, -self.bound(pos.move(move), 1 - gamma, depth - 1) # Run through the moves, shortcutting when possible best = -MATE_UPPER for move, score in moves(): best = max(best, score) if best >= gamma: # Save the move for pv construction and killer heuristic if move is not None: self.tp_move[pos] = move break # Stalemate checking is a bit tricky: Say we failed low, because # we can't (legally) move and so the (real) score is -infty. # At the next depth we are allowed to just return r, -infty <= r < gamma, # which is normally fine. # However, what if gamma = -10 and we don't have any legal moves? # Then the score is actaully a draw and we should fail high! # Thus, if best < gamma and best < 0 we need to double check what we are doing. # We will fix this problem another way: We add the requirement to bound, that # it always returns MATE_UPPER if the king is capturable. Even if another move # was also sufficient to go above gamma. If we see this value we know we are either # mate, or stalemate. It then suffices to check whether we're in check. # Note that at low depths, this may not actually be true, since maybe we just pruned # all the legal moves. So sunfish may report "mate", but then after more search # realize it's not a mate after all. That's fair. # This is too expensive to test at depth == 0 if depth > 2 and best == -MATE_UPPER: flipped = pos.rotate(nullmove=True) # Hopefully this is already in the TT because of null-move in_check = self.bound(flipped, MATE_UPPER, 0) == MATE_UPPER best = -MATE_LOWER if in_check else 0 # Table part 2 if best >= gamma: self.tp_score[pos, depth, can_null] = Entry(best, entry.upper) if best < gamma: self.tp_score[pos, depth, can_null] = Entry(entry.lower, best) return best def search(self, history): """Iterative deepening MTD-bi search""" self.nodes = 0 self.history = set(history) self.tp_score.clear() gamma = 0 # In finished games, we could potentially go far enough to cause a recursion # limit exception. Hence we bound the ply. We also can't start at 0, since # that's quiscent search, and we don't always play legal moves there. for depth in range(1, 1000): # The inner loop is a binary search on the score of the position. # Inv: lower <= score <= upper # 'while lower != upper' would work, but it's too much effort to spend # on what's probably not going to change the move played. lower, upper = -MATE_LOWER, MATE_LOWER while lower < upper - EVAL_ROUGHNESS: score = self.bound(history[-1], gamma, depth, can_null=False) if score >= gamma: lower = score if score < gamma: upper = score yield depth, gamma, score, self.tp_move.get(history[-1]) gamma = (lower + upper + 1) // 2 ############################################################################### # UCI User interface ############################################################################### def parse(c): fil, rank = ord(c[0]) - ord("a"), int(c[1]) - 1 return A1 + fil - 10 * rank def render(i): rank, fil = divmod(i - A1, 10) return chr(fil + ord("a")) + str(-rank + 1) hist = [Position(initial, 0, (True, True), (True, True), 0, 0)] #input = raw_input # minifier-hide start import sys, tools.uci tools.uci.run(sys.modules[__name__], hist[-1]) sys.exit() # minifier-hide end searcher = Searcher() while True: args = input().split() if args[0] == "uci": print("id name", version) print("uciok") elif args[0] == "isready": print("readyok") elif args[0] == "quit": break elif args[:2] == ["position", "startpos"]: del hist[1:] for ply, move in enumerate(args[3:]): i, j, prom = parse(move[:2]), parse(move[2:4]), move[4:].upper() if ply % 2 == 1: i, j = 119 - i, 119 - j hist.append(hist[-1].move(Move(i, j, prom))) elif args[0] == "go": wtime, btime, winc, binc = [int(a) / 1000 for a in args[2::2]] if len(hist) % 2 == 0: wtime, winc = btime, binc think = min(wtime / 40 + winc, wtime / 2 - 1) start = time.time() move_str = None for depth, gamma, score, move in Searcher().search(hist): # The only way we can be sure to have the real move in tp_move, # is if we have just failed high. if score >= gamma: i, j = move.i, move.j if len(hist) % 2 == 0: i, j = 119 - i, 119 - j move_str = render(i) + render(j) + move.prom.lower() print("info depth", depth, "score cp", score, "pv", move_str) if move_str and time.time() - start > think * 0.8: break print("bestmove", move_str or '(none)') 这段代码需要数据训练模型吗
11-12
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值