1 2# The purpose of this module, is to give a certain position a score. 3# The greater the score, the better the position 4 5from pychess.Utils.const import WHITE, BLACK, LOSERSCHESS, SUICIDECHESS, GIVEAWAYCHESS,\ 6 ASEAN_VARIANTS, ATOMICCHESS, CRAZYHOUSECHESS, RACINGKINGSCHESS, THREECHECKCHESS,\ 7 BPAWN, BISHOP, KNIGHT, QUEEN, KING, PAWN, ROOK, \ 8 CAS_FLAGS, H7, B6, A7, H2, G3, A2, B3, G6, D1, G8, B8, G1, B1 9from .bitboard import iterBits, firstBit, lsb 10from .ldata import fileBits, bitPosArray, PIECE_VALUES, FILE, RANK, PAWN_VALUE,\ 11 WHITE_SQUARES, BLACK_SQUARES, ASEAN_PIECE_VALUES, ATOMIC_PIECE_VALUES, CRAZY_PIECE_VALUES,\ 12 kwingpawns1, kwingpawns2, qwingpawns1, qwingpawns2, frontWall, endingKing,\ 13 brank7, brank8, distance, isolaniMask, d2e2, passedScores, squarePawnMask,\ 14 moveArray, brank67, lbox, stonewall, isolani_normal, isolani_weaker,\ 15 passedPawnMask, fromToRay, pawnScoreBoard, sdistance, taxicab, racingKing 16from .lsort import staticExchangeEvaluate 17from .lmovegen import newMove 18from pychess.Variants.threecheck import checkCount 19from pychess.Variants.racingkings import testKingInEightRow 20from ctypes import create_string_buffer, memset 21from struct import Struct 22 23# from random import randint 24randomval = 0 # randint(8,12)/10. 25 26CHECK_BONUS = (0, 100, 500, 2000) 27 28 29def evaluateComplete(board, color): 30 """ A detailed evaluation function, taking into account 31 several positional factors """ 32 33 s, phase = evalMaterial(board, color) 34 if board.variant in (LOSERSCHESS, SUICIDECHESS, GIVEAWAYCHESS): 35 return s 36 37 s += evalKing(board, color, phase) - evalKing(board, 1 - color, phase) 38 if board.variant == RACINGKINGSCHESS or board.variant == THREECHECKCHESS: 39 return s 40 41 s += evalBishops(board, color, phase) - evalBishops(board, 1 - color, 42 phase) 43 s += evalRooks(board, color, phase) - evalRooks(board, 1 - color, phase) 44 s += evalDoubleQR7(board, color, phase) - evalDoubleQR7(board, 1 - color, 45 phase) 46 s += evalKingTropism(board, color, phase) - evalKingTropism(board, 1 - 47 color, phase) 48 if board.variant in ASEAN_VARIANTS: 49 return s 50 s += evalDev(board, color, phase) - evalDev(board, 1 - color, phase) 51 if board.variant == ATOMICCHESS: 52 return s 53 pawnScore, passed, weaked = cacheablePawnInfo(board, phase) 54 s += pawnScore if color == WHITE else -pawnScore 55 s += evalPawnStructure(board, color, phase, passed, 56 weaked) - evalPawnStructure(board, 1 - color, phase, 57 passed, weaked) 58 59 s += evalTrappedBishops(board, color) 60 s += randomval 61 62 return s 63 64################################################################################ 65# evalMaterial # 66################################################################################ 67 68 69def evalMaterial(board, color): 70 # While opp reached rank 8 we can save the game if we also reach it 71 # but for this we have to force the shortest (one king move) draw line! 72 if board.variant == RACINGKINGSCHESS and testKingInEightRow(board): 73 return [0, 0] 74 75 pieceCount = board.pieceCount 76 opcolor = 1 - color 77 material = [0, 0] 78 if board.variant == CRAZYHOUSECHESS: 79 for piece in range(PAWN, KING): 80 material[WHITE] += CRAZY_PIECE_VALUES[piece] * pieceCount[WHITE][ 81 piece] 82 material[BLACK] += CRAZY_PIECE_VALUES[piece] * pieceCount[BLACK][ 83 piece] 84 material[WHITE] += CRAZY_PIECE_VALUES[piece] * board.holding[ 85 WHITE][piece] 86 material[BLACK] += CRAZY_PIECE_VALUES[piece] * board.holding[ 87 BLACK][piece] 88 elif board.variant == LOSERSCHESS: 89 for piece in range(PAWN, KING): 90 material[WHITE] += pieceCount[WHITE][piece] 91 material[BLACK] += pieceCount[BLACK][piece] 92 elif board.variant == SUICIDECHESS or board.variant == GIVEAWAYCHESS: 93 for piece in range(PAWN, KING + 1): 94 material[WHITE] += pieceCount[WHITE][piece] 95 material[BLACK] += pieceCount[BLACK][piece] 96 elif board.variant == ATOMICCHESS: 97 for piece in range(PAWN, KING + 1): 98 material[WHITE] += ATOMIC_PIECE_VALUES[piece] * pieceCount[WHITE][ 99 piece] 100 material[BLACK] += ATOMIC_PIECE_VALUES[piece] * pieceCount[BLACK][ 101 piece] 102 elif board.variant in ASEAN_VARIANTS: 103 for piece in range(PAWN, KING + 1): 104 material[WHITE] += ASEAN_PIECE_VALUES[piece] * pieceCount[WHITE][ 105 piece] 106 material[BLACK] += ASEAN_PIECE_VALUES[piece] * pieceCount[BLACK][ 107 piece] 108 else: 109 for piece in range(PAWN, KING): 110 material[WHITE] += PIECE_VALUES[piece] * pieceCount[WHITE][piece] 111 material[BLACK] += PIECE_VALUES[piece] * pieceCount[BLACK][piece] 112 113 phase = max(1, 8 - (material[WHITE] + material[BLACK]) // 1150) 114 115 # If both sides are equal, we don't need to compute anything! 116 if material[BLACK] == material[WHITE]: 117 return 0, phase 118 119 matTotal = sum(material) 120 121 # Who is leading the game, material-wise? 122 if material[color] > material[opcolor]: 123 leading = color 124 else: 125 leading = opcolor 126 127 if board.variant in (LOSERSCHESS, SUICIDECHESS, GIVEAWAYCHESS): 128 val = material[leading] - material[1 - leading] 129 val = int(100 * PAWN_VALUE * val / max(material[WHITE], material[BLACK])) 130 if leading == 1 - color: 131 return val, phase 132 return -val, phase 133 134 pawns = pieceCount[leading][PAWN] 135 matDiff = material[leading] - material[1 - leading] 136 val = min(2400, matDiff) + (matDiff * (12000 - matTotal) * pawns) // (6400 * (pawns + 1)) 137 138 if leading == color: 139 return val, phase 140 return -val, phase 141 142 ################################################################################ 143 # evalKingTropism # 144 ################################################################################ 145 146 147pawnTropism = [[0] * 64 for i in range(64)] 148bishopTropism = [[0] * 64 for i in range(64)] 149knightTropism = [[0] * 64 for i in range(64)] 150rookTropism = [[0] * 64 for i in range(64)] 151queenTropism = [[0] * 64 for i in range(64)] 152 153for pcord in range(64): 154 for kcord in range(pcord + 1, 64): 155 pawnTropism[pcord][kcord] = pawnTropism[kcord][pcord] = \ 156 (14 - taxicab[pcord][kcord])**2 * 10 / 169 157 knightTropism[pcord][kcord] = knightTropism[kcord][pcord] = \ 158 (6 - distance[KNIGHT][pcord][kcord])**2 * 2 159 bishopTropism[pcord][kcord] = bishopTropism[kcord][pcord] = \ 160 (14 - distance[BISHOP][pcord][kcord] * sdistance[pcord][kcord])**2 * 30 // 169 161 rookTropism[pcord][kcord] = rookTropism[kcord][pcord] = \ 162 (14 - distance[ROOK][pcord][kcord] * sdistance[pcord][kcord])**2 * 40 // 169 163 queenTropism[pcord][kcord] = queenTropism[kcord][pcord] = \ 164 (14 - distance[QUEEN][pcord][kcord] * sdistance[pcord][kcord])**2 * 50 // 169 165 166tropisms = { 167 PAWN: pawnTropism, 168 KNIGHT: knightTropism, 169 BISHOP: bishopTropism, 170 ROOK: rookTropism, 171 QUEEN: queenTropism 172} 173 174 175def evalKingTropism(board, color, phase): 176 """ All other things being equal, having your Knights, Queens and Rooks 177 close to the opponent's king is a good thing """ 178 _tropisms = tropisms 179 _lsb = lsb 180 opcolor = 1 - color 181 pieces = board.boards[color] 182 183 opking = board.kings[opcolor] 184 185 score = 0 186 for piece in range(KNIGHT, KING): 187 # for piece in range(PAWN, KING): 188 bitboard = pieces[piece] 189 tropism = _tropisms[piece] 190 # inlined iterBits() 191 while bitboard: 192 bit = bitboard & -bitboard 193 score += tropism[_lsb[bit]][opking] 194 bitboard -= bit 195 return score 196 197 198################################################################################ 199# evalPawnStructure # 200################################################################################ 201 202# For pawn hash, don't use buckets. Store: 203# key high 16 bits of pawn hash key 204# score score from white's point of view 205# passed bitboard of passed pawns 206# weaked bitboard of weak pawns 207pawnEntryType = Struct('=H h Q Q') 208PAWN_HASH_SIZE = 16384 209PAWN_PHASE_KEY = (0x343d, 0x055d, 0x3d3c, 0x1a1c, 0x28aa, 0x19ee, 0x1538, 210 0x2a99) 211pawntable = create_string_buffer(PAWN_HASH_SIZE * pawnEntryType.size) 212 213 214def clearPawnTable(): 215 memset(pawntable, 0, PAWN_HASH_SIZE * pawnEntryType.size) 216 217 218def probePawns(board, phase): 219 index = (board.pawnhash % PAWN_HASH_SIZE) ^ PAWN_PHASE_KEY[phase - 1] 220 key, score, passed, weaked = pawnEntryType.unpack_from(pawntable, index * 221 pawnEntryType.size) 222 if key == (board.pawnhash >> 14) & 0xffff: 223 return score, passed, weaked 224 return None 225 226 227def recordPawns(board, phase, score, passed, weaked): 228 index = (board.pawnhash % PAWN_HASH_SIZE) ^ PAWN_PHASE_KEY[phase - 1] 229 key = (board.pawnhash >> 14) & 0xffff 230 pawnEntryType.pack_into(pawntable, index * pawnEntryType.size, key, score, 231 passed, weaked) 232 233 234def cacheablePawnInfo(board, phase): 235 entry = probePawns(board, phase) 236 if entry: 237 return entry 238 239 score = 0 240 passed = 0 241 weaked = 0 242 243 for color in WHITE, BLACK: 244 opcolor = 1 - color 245 pawns = board.boards[color][PAWN] 246 oppawns = board.boards[opcolor][PAWN] 247 248 nfile = [0] * 8 249 pScoreBoard = pawnScoreBoard[color] 250 for cord in iterBits(pawns): 251 score += pScoreBoard[cord] * 2 252 253 # Passed pawns 254 if not oppawns & passedPawnMask[color][cord]: 255 if (color == WHITE and not fromToRay[cord][cord | 56] & pawns) or\ 256 (color == BLACK and not fromToRay[cord][cord & 7] & pawns): 257 passed |= bitPosArray[cord] 258 score += (passedScores[color][cord >> 3] * phase) // 12 259 260 # Backward pawns 261 backward = False 262 263 if color == WHITE: 264 i = cord + 8 265 else: 266 i = cord - 8 267 268 ptype = color == WHITE and PAWN or BPAWN 269 opptype = color == BLACK and PAWN or BPAWN 270 271 if not (passedPawnMask[opcolor][i] & ~fileBits[cord & 7] & pawns) and\ 272 board.arBoard[i] != PAWN: 273 n1 = bin(pawns & moveArray[opptype][i]).count("1") 274 n2 = bin(oppawns & moveArray[ptype][i]).count("1") 275 if n1 < n2: 276 backward = True 277 278 if not backward and bitPosArray[cord] & brank7[opcolor]: 279 i = i + (color == WHITE and 8 or -8) 280 if not (passedPawnMask[opcolor][i] & ~fileBits[1] & pawns): 281 n1 = bin(pawns & moveArray[opptype][i]).count("1") 282 n2 = bin(oppawns & moveArray[ptype][i]).count("1") 283 if n1 < n2: 284 backward = True 285 286 if not backward and bitPosArray[cord] & brank7[opcolor]: 287 i = i + (color == WHITE and 8 or -8) 288 if not (passedPawnMask[opcolor][i] & ~fileBits[1] & pawns): 289 n1 = bin(pawns & moveArray[opptype][i]).count("1") 290 n2 = bin(oppawns & moveArray[ptype][i]).count("1") 291 if n1 < n2: 292 backward = True 293 294 if backward: 295 weaked |= bitPosArray[cord] 296 score += -(8 + phase) # Backward pawn penalty 297 298 # Pawn base under attack 299 if moveArray[ptype][cord] & oppawns and \ 300 moveArray[ptype][cord] & pawns: 301 score += -18 302 303 # Increment file count for isolani & doubled pawn evaluation 304 nfile[cord & 7] += 1 305 306 for i in range(8): 307 # Doubled pawns 308 if nfile[i] > 1: 309 score += -(8 + phase) 310 311 # Isolated pawns 312 if nfile[i] and not pawns & isolaniMask[i]: 313 if not fileBits[i] & oppawns: 314 # Isolated on a half-open file 315 score += isolani_weaker[i] * nfile[i] 316 else: 317 # Normal isolated pawn 318 score += isolani_normal[i] * nfile[i] 319 weaked |= pawns & fileBits[i] 320 321 # Penalize having eight pawns 322 if board.pieceCount[color][PAWN] == 8: 323 score -= 10 324 325 # Detect stonewall formation in our pawns 326 if stonewall[color] & pawns == stonewall[color]: 327 score += 10 328 329 # Penalize Locked pawns 330 n = bin((pawns >> 8) & oppawns & lbox).count("1") 331 score -= n * 10 332 333 # Switch point of view when switching colors 334 score = -score 335 336 recordPawns(board, phase, score, passed, weaked) 337 return score, passed, weaked 338 339 340def evalPawnStructure(board, color, phase, passed, weaked): 341 """ 342 Pawn evaluation is based on the following factors: 343 1. Pawn square tables. 344 2. Passed pawns. 345 3. Backward pawns. 346 4. Pawn base under attack. 347 5. Doubled pawns 348 6. Isolated pawns 349 7. Connected passed pawns on 6/7th rank. 350 8. Unmoved & blocked d, e pawn 351 9. Passed pawn which cannot be caught. 352 10. Pawn storms. 353 Notice: The function has better precicion for current player 354 """ 355 356 boards = board.boards[color] 357 358 if not boards[PAWN]: 359 return 0 360 361 king = board.kings[color] 362 pawns = boards[PAWN] 363 364 opcolor = 1 - color 365 opking = board.kings[opcolor] 366 opboards = board.boards[opcolor] 367 368 score = 0 369 passed &= pawns 370 weaked &= pawns 371 372 # This section of the pawn code cannot be saved into the pawn hash as 373 # they depend on the position of other pieces. So they have to be 374 # calculated again. 375 if passed: 376 # Connected passed pawns on 6th or 7th rank 377 t = passed & brank67[color] 378 opMajorCount = 0 379 for p in range(KNIGHT, KING): 380 opMajorCount += board.pieceCount[opcolor][p] 381 if t and opMajorCount == 1: 382 n1 = FILE(opking) 383 n2 = RANK(opking) 384 for f in range(7): 385 if t & fileBits[f] and t & fileBits[f + 1] and \ 386 (n1 < f - 1 or n1 > f + 1 or (color == WHITE and n2 < 4) or 387 (color == BLACK and n2 > 3)): 388 score += 50 389 390 # Enemy has no pieces & King is outcolor of passed pawn square 391 if not opMajorCount: 392 for cord in iterBits(passed): 393 if board.color == color: 394 if not squarePawnMask[color][cord] & opboards[KING]: 395 score += passedScores[color][RANK(cord)] 396 else: 397 if not moveArray[KING][opking] & squarePawnMask[color][ 398 cord]: 399 score += passedScores[color][RANK(cord)] 400 401 # Estimate if any majors are able to hunt us down 402 for pawn in iterBits(passed): 403 found_hunter = False 404 if color == WHITE: 405 prom_cord = 7 << 3 | FILE(pawn) 406 else: 407 prom_cord = FILE(pawn) 408 distance_to_promotion = distance[PAWN][pawn][prom_cord] 409 for piece in range(KNIGHT, KING + 1): 410 for cord in iterBits(opboards[piece]): 411 hunter_distance = distance[piece][cord][prom_cord] 412 if hunter_distance <= distance_to_promotion: 413 found_hunter = True 414 break 415 if found_hunter: 416 break 417 if not found_hunter: 418 score += passedScores[color][RANK(pawn)] // 5 419 420 # Penalize Pawn on d2,e2/d7,e7 is blocked 421 blocker = board.blocker 422 if color == WHITE and ((pawns & d2e2[WHITE]) >> 8) & blocker: 423 score -= 48 424 elif color == BLACK and ((pawns & d2e2[BLACK]) << 8) & blocker: 425 score -= 48 426 427 # If both colors are castled on different colors, bonus for pawn storms 428 if abs(FILE(king) - FILE(opking)) >= 4 and phase < 6: 429 n1 = FILE(opking) 430 p = (isolaniMask[n1] | fileBits[n1]) & pawns 431 score += sum(10 * (5 - distance[KING][c][opking]) for c in iterBits(p)) 432 433 return score 434 435 436# evalBateries 437def evalDoubleQR7(board, color, phase): 438 """ Tests for QR, RR, QB and BB combos on the 7th rank. These are dangerous 439 to kings, and good at killing pawns """ 440 441 opcolor = 1 - board.color 442 boards = board.boards[color] 443 opboards = board.boards[opcolor] 444 445 if bin((boards[QUEEN] | boards[ROOK]) & brank7[color]).count("1") >= 2 and \ 446 (opboards[KING] & brank8[color] or opboards[PAWN] & brank7[color]): 447 return 30 448 449 return 0 450 451 452def evalKing(board, color, phase): 453 # Should avoid situations like those: 454 # r - - - n K - - 455 # which makes forks more easy 456 # and 457 # R - - - K - - - 458 # and 459 # - - - - - - - - 460 # - - - K - - - - 461 # - - - - - - - - 462 # - - - - - - - - 463 # - - - - - - B - 464 # which might turn bad 465 466 # Also being check should be avoided, like 467 # - q - - - K - r 468 # and 469 # - - - - - n - - 470 # - - - K - - - R 471 472 king = board.kings[color] 473 474 if board.variant == RACINGKINGSCHESS: 475 return racingKing[king] 476 477 if board.variant == THREECHECKCHESS: 478 return CHECK_BONUS[min(3, checkCount(board, 1 - color))] 479 480 # If we are in endgame, we want our king in the center, and theirs far away 481 if phase >= 6: 482 return endingKing[king] 483 484 # else if castled, prefer having some pawns in front 485 elif FILE(king) not in (3, 4) and RANK(king) in (0, 8): 486 if color == WHITE: 487 if FILE(king) < 3: 488 wall1 = frontWall[color][B1] 489 else: 490 wall1 = frontWall[color][G1] 491 wall2 = wall1 >> 8 492 else: 493 if FILE(king) < 3: 494 wall1 = frontWall[color][B8] 495 else: 496 wall1 = frontWall[color][G8] 497 wall2 = wall1 << 8 498 499 pawns = board.boards[color][PAWN] 500 total_in_front = bin(wall1 | wall2 & pawns).count("1") 501 numbermod = (0, 3, 6, 9, 7, 5, 3)[total_in_front] 502 503 s = bin(wall1 & pawns).count("1") * 2 + bin(wall2 & pawns).count("1") 504 return (s * numbermod * 5) // 6 505 506 return 0 507 508 509def evalDev(board, color, phase): 510 """ 511 Calculate the development score for side (for opening only). 512 Penalize the following. 513 . Uncastled and cannot castled 514 . Early queen move. 515 - bad wing pawns 516 """ 517 518 # If we are castled or beyond the 20th move, no more evalDev 519 520 if board.plyCount >= 38: 521 return 0 522 523 score = 0 524 525 if not board.hasCastled[color]: 526 527 boards = board.boards[color] 528 pawns = boards[PAWN] 529 530 # We don't encourage castling, but it should always be possible 531 if not board.castling & CAS_FLAGS[color][0]: 532 score -= 40 533 if not board.castling & CAS_FLAGS[color][1]: 534 score -= 50 535 536 # Should keep queen home 537 cord = firstBit(boards[QUEEN]) 538 if cord != D1 + 56 * color: 539 score -= 30 540 541 qpawns = max(qwingpawns1[color] & pawns, qwingpawns2[color] & pawns) 542 kpawns = max(kwingpawns1[color] & pawns, kwingpawns2[color] & pawns) 543 544 if qpawns != 2 and kpawns != 2: 545 # Structure destroyed in both sides 546 score -= 35 547 else: 548 # Discourage any wing pawn moves 549 score += (qpawns + kpawns) * 6 550 551 return score 552 553 554def evalBishops(board, color, phase): 555 556 opcolor = 1 - color 557 bishops = board.boards[color][BISHOP] 558 if not bishops: 559 return 0 560 561 pawns = board.boards[color][PAWN] 562 oppawns = board.boards[opcolor][PAWN] 563 564 score = 0 565 566 # Avoid having too many pawns on you bishop's color. 567 # In late game phase, add a bonus for enemy pieces on your bishop's color. 568 569 if board.pieceCount[color][BISHOP] == 1: 570 squareMask = WHITE_SQUARES if (bishops & 571 WHITE_SQUARES) else BLACK_SQUARES 572 score = - bin(pawns & squareMask).count("1") \ 573 - bin(oppawns & squareMask).count("1") // 2 574 if phase > 6: 575 score += bin(board.friends[1 - color] & squareMask).count("1") 576 577 return score 578 579 580def evalTrappedBishops(board, color): 581 """ Check for bishops trapped at A2/H2/A7/H7 """ 582 583 _bitPosArray = bitPosArray 584 wbishops = board.boards[WHITE][BISHOP] 585 bbishops = board.boards[BLACK][BISHOP] 586 wpawns = board.boards[WHITE][PAWN] 587 bpawns = board.boards[BLACK][PAWN] 588 score = 0 589 590 if bbishops: 591 if bbishops & _bitPosArray[A2] and wpawns & _bitPosArray[B3]: 592 see = staticExchangeEvaluate(board, newMove(A2, B3)) 593 if see < 0: 594 score -= see 595 if bbishops & _bitPosArray[H2] and wpawns & _bitPosArray[G3]: 596 see = staticExchangeEvaluate(board, newMove(H2, G3)) 597 if see < 0: 598 score -= see 599 600 if wbishops: 601 if wbishops & _bitPosArray[A7] and bpawns & _bitPosArray[B6]: 602 see = staticExchangeEvaluate(board, newMove(A7, B6)) 603 if see < 0: 604 score += see 605 if wbishops & _bitPosArray[H7] and bpawns & _bitPosArray[G6]: 606 see = staticExchangeEvaluate(board, newMove(H7, G6)) 607 if see < 0: 608 score += see 609 610 return score if color == WHITE else -score 611 612 613def evalRooks(board, color, phase): 614 """ rooks on open/half-open files """ 615 616 opcolor = 1 - color 617 boards = board.boards[color] 618 rooks = boards[ROOK] 619 620 if not rooks: 621 return 0 622 623 opking = board.kings[opcolor] 624 score = 0 625 626 if phase < 7: 627 for cord in iterBits(rooks): 628 file = cord & 7 629 if not boards[PAWN] & fileBits[file]: 630 if file == 5 and opking & 7 >= 4: 631 score += 40 632 score += 5 633 if not boards[PAWN] & fileBits[file]: 634 score += 6 635 636 return score 637