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