# #---PyChess Module "game". Definitions of an instantaneous chess game state. # # Classes: # Square # __init__ (square_number) { only used by GameState()'s __init__ } # remove_attacker (piece) remove from list of attackers of this square # add_attacker (piece) add... to list ... # moves_along (angle) returns [] of moves along an angle # # GameState: owns the entire instantaneous "state" of a game, including # the array of Square's and pieces, and game ply depth and # search depth information. # # __init__() creates new game state with empty board & no pieces # # printme () crude printout of board with some attack info # lift (piece) "lift" a piece off the board # place (piece) "place" a piece onto the board # pinned (piece) returns [pinned1, pinner1, pinned2, pinner2...] # # specific_pieces (machine, [type1, type2,...]) # returns [] of pieces by type # # newgame (mwhite) # fills (new) game state with pieces in traditional starting places # # Public Functions: # angle_from (a, b) returns angle number from square number a to # square number b. # # Globals: # bad_angle illegal angle (square to square) # # Notes: could eventually precompute Square.moves_along as a 2-d array for # all possible squares -- considerable time savings? # # Revision history: #:CR 07/25/00 First version. #--------------------------------------------------------------------------- import piece #=====Square class========================================================== #---Some constants, tables, and functions used inside class Square for # manipulating square numbers. _move_in = (10, 21, 11, 12, 1, -8, -9, -19, -10, -21, -11, -12, -1, 8, 9, 19) _in8x8 = [0] * 120 for y in range(2,10): for x in range(1,9): _in8x8[y*10 + x] = 1 def _onboard (sn): return _in8x8[sn + 20] class Square: def __init__ (self, num): self.piece = None # piece occupying this square self.num = num # square's "square number" self.ackers = [ [], [] ] # [] of opponent, machine pieces attacking sq def attackstr (self): value = "%d%d " % (len(self.ackers[1]), len(self.ackers[0])) if value == "00 ": value = " " return value def remove_attacker(self, p): self.ackers[p.machine].remove(p) def add_attacker (self, p): self.ackers[p.machine].append(p) def moves_along (self, angle): moves = [] sq = self.num + _move_in[angle] while _onboard(sq): moves.append(sq) sq = sq + _move_in[angle] return moves #=====GameState class======================================================== class GameState: def __init__ (self): self.sqr = [0] * 79 for i in range(1,79): self.sqr[i] = Square(i) self.ply = 0 # ply number in game; do not update in searches! self.pieces = [ [], [] ] self.mwhite = 0 # 1=>machine is white, 0 otherwise self.tomove = 0 self.lastmoved = None self.branchdepth = 5 # default value, branch search down to ply 5 self.twigdepth = 12 # default value, search twigs down to ply 12 self.searchwidth = [15, 14] + ([13] * 30) # search widths at each level self.leafcount = 0 # counter for # leaves visited self.twigcoung = 0 # counter for # twigs visited def printme (self): for y in range(7,-1,-1): prow = "" arow = "" nrow = "" for x in range(1,9): s = y*10 + x nrow = nrow + "%2d " % s p = self.sqr[s].piece if p == None: if (x+y) % 2: prow = prow + "** " else: prow = prow + "-- " else: prow = prow + " " + p.letter() + " " arow = arow + self.sqr[s].attackstr() print prow + " " + arow + " " + nrow #================================================================== #---GameState.lift (p) Lift piece P from the board. def lift (self, p): s = p.sn if p.ispawn: self.lift_pawn (p) else: self.lift_piece (p) for q in self.sqr[s].ackers[1]: self.lift_adjust (q, s) for q in self.sqr[s].ackers[0]: self.lift_adjust (q, s) self.adjust_unblocked_pawns (s) def lift_pawn (self, p): #---"Unattack" pawn's diagonal squares s = p.sn for angle in p.angles: for sq in self.sqr[s].moves_along (angle)[0:1]: self.sqr[sq].remove_attacker(p) p.covers[angle] = None #---Clear P's move list & sqr #, remove it from the square p.line[p.move] = [] p.sn = 0 self.sqr[s].piece = None def lift_piece (self, p): #---Step a: remove P from the board. self.sqr[p.sn].piece = None p.sn = 0 for angle in p.angles: #---Step b: for each square X that P could move to, remove P # from the list of pieces attacking X. for x in p.line[angle]: self.sqr[x].remove_attacker(p) #---Step c: if P covers a piece Q in this angle, remove P # from list of pieces attacking Q's square. q = p.covers[angle] if q != None: self.sqr[q.sn].remove_attacker(p) #---Step d: clear covers entries and move list. p.covers[angle] = None p.line [angle] = [] # #---lift-adjust (q, s). Q used to attack a piece on S. Adjust # piece Q's info after piece removed from S. def lift_adjust (self, q, s): angle = angle_from (q.sn, s) #---Q no longer cover's piece formerly at S. q.covers[angle] = None #---Add S to Q's move list. if not q.ispawn: q.line[angle].append(s) #---"Ranger" pieces continue down ANGLE, marking squares SQ # as attacked by Q, and adding SQ to Q's move list. if q.ranger: for sq in self.sqr[s].moves_along (angle): self.sqr[sq].add_attacker(q) #---If there's a piece here, mark it as covered by Q, and stop. r = self.sqr[sq].piece if r != None: q.covers[angle] = r break #---Otherwise, add SQ to Q's move list. q.line[angle].append(sq) # # #---Adjust pawns whose moves were unblocked by removing a piece from S. def adjust_unblocked_pawns (self, s): #---From S, scan up and down: look at (only) 1st two squares. # If either contains a pawn, refigure their movelist. for angle in (0, 8): for sq in self.sqr[s].moves_along(angle)[0:2]: p = self.sqr[sq].piece if p != None: if p.ispawn and p.move == 8-angle: self.figure_pawn (p) break # # #================================================================== #---GameState.place (s, p) On square S, place piece P. # Place updates the board and the pieces in three steps: # (a) Put P on the board, and update P's lists. (Place_piece) # (b) Adjust info for other pieces that attacked S. (Place_adjust) # (c) Trim move lists of pawns now blocked by P. def place (self, s, p): self.place_piece (s, p) if p.ispawn: self.figure_pawn (p) for q in self.sqr[s].ackers[1]: self.place_adjust (q, p, s) for q in self.sqr[s].ackers[0]: self.place_adjust (q, p, s) self.adjust_blocked_pawns (s) #---place_piece (s, p) # (a) Mark S as occupied by P # (b) Mark P as occupying S. # (c) For each of P's angles, compute squares P can move to; # add to P's list and mark squares as attacked by P. # (d) For each piece on squares from (c), mark them as attacked or # defended by P. def place_piece (self, s, p): self.sqr[s].piece = p # (step a) p.sn = s # (step b) for angle in p.angles: for sq in self.sqr[s].moves_along(angle): #---Add P to pieces attacking SQ -- (step c) self.sqr[sq].add_attacker(p) #---If a piece TARGET is on square SQ, mark it as covered. # Stop there. target = self.sqr[sq].piece # (step d) if target: p.covers[angle] = target break #---Otherwise, add SQ to P's list of moves. (finish step c) if p.ispawn: break p.line[angle].append (sq) if not p.ranger: break # # #---figure_pawn (p) Figure moves of newly placed pawn. def figure_pawn (self, p): angle = p.move sq = self.sqr[p.sn].moves_along(angle) p.line[angle] = [] #---Check square immediately in front of P. if self.sqr[sq[0]].piece != None: return p.line[angle].append(sq[0]) #---If on home rank, check 2-square option. if p.home != p.sn/10: return if self.sqr[sq[1]].piece != None: return p.line[angle].append(sq[1]) #---place_adjust (q, p, s) Adjust other pieces Q that attacked S. # Assume Q attacks S. P was just placed on S. Consider the line of # attack from Q to S. Q may have attacked squares X *beyond* S. # Q can no longer attack or move to any of X. Record this by: # (a) Mark squares X as not attacked by Q. # (b) Remove S and X from Q's movelist. # (c) If Q attacked a piece R beyond (P at) S, undo that. # (d) Add P to Q's attack/defend list. def place_adjust (self, q, p, s): angle = angle_from (q.sn, s) moves = q.line[angle] try: #---Step a: Mark X as not attacked by Q. si = moves.index(s) for x in moves[si+1:]: self.sqr[x].remove_attacker(q) #---Step b: remove S + X from Q's movelist q.line[angle] = moves[0:si] #---Step c: if Q attacks a piece R (beyond S). Remove Q from list of # pieces attacking R's square. Clear Q's a/d in this angle. r = q.covers[angle] if r != None: self.sqr[r.sn].remove_attacker(q) q.covers[angle] = None except: pass #---Step (d): Mark Q as attacking/defending P. q.covers[angle] = p #---adjust_blocked_pawns (s). Placing a piece at S may block # the forward movement of pawns above or below S. def adjust_blocked_pawns (self, s): #---From S, scan up and down: look at (only) 1st two squares. # If either contains a pawn, find S in their movelist, and truncate it. for angle in (0, 8): for sq in self.sqr[s].moves_along(angle)[0:2]: p = self.sqr[sq].piece if p != None and p.ispawn: try: x = p.line[8-angle].index(s) p.line[8-angle] = p.line[8-angle][0:x] break except: pass # # # #================================================================== #---GameState.pinned (p). Look for pieces "pinned" against P. # Returns list of (pinned1, pinner1, pinned2, pinner2...) def pinned (self, p): pinlist = [] for angle in (0, 2, 4, 6, 8, 10, 12, 14): #---block is the piece pinned against P. Step out along angle... block = None for s in self.sqr[p.sn].moves_along (angle): q = self.sqr[s].piece if q == None: continue same = p.machine == q.machine #---The first same colored piece becomes BLOCK. If a 2nd is # found, there's no pin. Ditto if we find an opposite color # piece, but no same color. if same and block == None: block = q elif same: break elif not same and block == None: break #---Bingo! Now if Q can move to attack P, we've got a pin. elif (angle in q.angles) and q.ranger: pinlist.append (block) pinlist.append (q) #---If not, there's no pin along this angle. else: break # # return pinlist #================================================================== #---GameState.specific_pieces() return list of pieces matching [types...] def specific_pieces (self, machine, type): result = [] for p in self.pieces[machine]: if p.type in type: result.append(p) return result #---GameState.newgame() # Set up a new game, creating all the pieces and putting them # in the traditional position. Must be invoked on a newly-created # GameState object. Mwhite == machine plays white. def newgame (self, mwhite): self.mwhite = mwhite self.tomove = mwhite self.ply = 0 self.newpiece ( 4+mwhite, piece.King(1)) self.newpiece ( 5-mwhite, piece.Queen(1)) self.newpiece ( 1, piece.Rook(1)) self.newpiece ( 8, piece.Rook(1)) self.newpiece ( 3, piece.Bishop(1)) self.newpiece ( 6, piece.Bishop(1)) self.newpiece ( 2, piece.Knight(1)) self.newpiece ( 7, piece.Knight(1)) for s in range(11,19): self.newpiece (s, piece.Mpawn(1)) self.newpiece (74+mwhite, piece.King(0)) self.newpiece (75-mwhite, piece.Queen(0)) self.newpiece (71, piece.Rook(0)) self.newpiece (78, piece.Rook(0)) self.newpiece (73, piece.Bishop(0)) self.newpiece (76, piece.Bishop(0)) self.newpiece (72, piece.Knight(0)) self.newpiece (77, piece.Knight(0)) for s in range(61,69): self.newpiece (s, piece.Opawn(0)) def newpiece (self, s, p): self.pieces[p.machine].append(p) self.place (s, p) #---Make a move on the current gamestate. def makemove (self, move): #---Makemove is pretty straight-forward. In general, all it has # to do is LIFT the piece being moved, and PLACE it on the # new square. Captured pieces must be lifted and removed. # The only special cases are castling (rook must be moved) # and pawn promotion. s0 = move.p.sn s1 = move.sn #---Castling. if move.p.type == piece.king and move.note: self.lift (move.p) self.lift (move.note) self.place (s1, move.p) self.place ((s0+s1)/2, move.note) move.p.flag = 2 # special flags for castling move.note.flag = 1 # # /*** Pawn promotion. */ # else if (piece[p].type >= MP && move->note > 0) { # if (move->capture > 0) remove (move->capture, piece, square); # remove (p, piece, square); # piece[p].type = move->note; # place (p, s1, piece, square); # piece[p].flag = 1; # } #---Other regular moves. else: if move.capture: self.pieces[move.capture.machine].remove (move.capture) self.lift (move.capture) self.lift (move.p) self.place (move.sn, move.p) move.p.flag = 1 # piece has moved #---Clear all pawn movement flags. for p in self.pieces[move.p.machine]: if p.type in (piece.mpawn, piece.opawn): p.flag = 0 #---If a pawn just "took its option" to move 2 squares, flag it. if (move.p.type in (piece.mpawn, piece.opawn) and abs(s1-s0) == 20) : move.p.flag = 1 #---Update stats relating to move just made self.tomove = 1 - self.tomove self.ply = self.ply + 1 self.lastmoved = move.p return #=====angle_from function=============================================== # BIGBOARD maps squares on the 8 x 10 board into a 16 x 10 board used # exclusively to index into delta_angle and the angle_from function. _bigboard = ( 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121) bad_angle = 17 _delta = [bad_angle] * 239 for i in range ( 16, 113, 16): _delta[i+119] = 0 # up for i in range (-112, 0, 16): _delta[i+119] = 8 # down for i in range ( 1, 8, 1): _delta[i+119] = 4 # right for i in range ( -7, 0, 1): _delta[i+119] = 12 # left for i in range ( 17, 120, 17): _delta[i+119] = 2 # up/right for i in range (-119, 0, 17): _delta[i+119] = 10 # down/left for i in range ( 15, 106, 15): _delta[i+119] = 14 # up/left for i in range (-105, 0, 15): _delta[i+119] = 6 # down/right for (i,d) in ((33,1), (18,3), (-14,5), (-31,7), (-33,9), (-18,11), (14,13), (31,15)): _delta[i+119] = d # knight moves def angle_from (a,b): return _delta [ _bigboard[b] - _bigboard[a] + 119] #=====================================================================