1# -*- coding: UTF-8 -*-
2
3from .ldata import bitPosArray, fileBits, rankBits, moveArray
4from .bitboard import firstBit, iterBits
5from .validator import validateMove
6
7from pychess.Utils.const import SAN, AN, LAN, ENPASSANT, EMPTY, PAWN, KING_CASTLE, QUEEN_CASTLE,\
8    reprFile, reprRank, chr2Sign, cordDic, reprSign, reprCord, reprSignSittuyin, reprSignMakruk,\
9    QUEEN, KNIGHT, BISHOP, ROOK, KING, NORMALCHESS, NORMAL_MOVE, PROMOTIONS, WHITE, BLACK, DROP,\
10    FAN_PIECES, SITTUYINCHESS, FISCHERRANDOMCHESS, SUICIDECHESS, MAKRUKCHESS, CAMBODIANCHESS,\
11    GIVEAWAYCHESS, ATOMICCHESS, WILDCASTLECHESS, WILDCASTLESHUFFLECHESS, HORDECHESS,\
12    chrU2Sign, CASTLE_KR, CASTLE_SAN, QUEEN_PROMOTION, NULL_MOVE, FAN, ASEAN_QUEEN
13from pychess.Utils.repr import reprPiece, localReprSign
14from pychess.Utils.lutils.lmovegen import genAllMoves, genPieceMoves, newMove
15
16
17def RANK(cord):
18    return cord >> 3
19
20
21def FILE(cord):
22    return cord & 7
23
24
25def TCORD(move):
26    return move & 63
27
28
29def FCORD(move):
30    return move >> 6 & 63
31
32
33def FLAG(move):
34    return move >> 12
35
36
37def PROMOTE_PIECE(flag):
38    return flag - 2
39
40
41def FLAG_PIECE(piece):
42    return piece + 2
43
44
45class ParsingError(Exception):
46    """ Please raise this with a 3-tupple: (move, reason, board.asFen())
47        The reason should be usable in the context: 'Move was not parseable
48        because %s' % reason """
49    pass
50
51
52def sittuyin_promotion_fcord(board, tcord):
53    queenMoves = moveArray[ASEAN_QUEEN]
54    for fcord in iterBits(queenMoves[tcord]):
55        if board.arBoard[fcord]:
56            return fcord
57
58################################################################################
59# parseAny                                                                     #
60################################################################################
61
62
63def parseAny(board, algnot):
64    type = determineAlgebraicNotation(algnot)
65    if type == SAN:
66        return parseSAN(board, algnot)
67    if type == AN:
68        return parseAN(board, algnot)
69    if type == LAN:
70        return parseLAN(board, algnot)
71    return parseFAN(board, algnot)
72
73
74def determineAlgebraicNotation(algnot):
75    upnot = algnot.upper()
76    if upnot in ("O-O", "O-O-O", "0-0", "0-0-0", "OO", "OOO", "00", "000"):
77        return SAN
78
79    # Test for e2-e4
80    if "-" in algnot:
81        return LAN
82
83    # Test for b4xc5
84    if "x" in algnot and algnot.split('x')[0] in cordDic:
85        return LAN
86
87    # Test for e2e4 or a7a8q or a7a8=q
88    if algnot[:2] in cordDic and algnot[2:4] in cordDic:
89        return AN
90
91    if algnot[0] in FAN_PIECES[WHITE] or algnot[0] in FAN_PIECES[BLACK]:
92        return FAN
93
94    return SAN
95
96################################################################################
97# listToSan                                                                    #
98################################################################################
99
100
101def listToSan(board, moves):
102    # Work on a copy to ensure we don't break things
103    board = board.clone()
104    sanmoves = []
105    for move in moves:
106        san = toSAN(board, move)
107        sanmoves.append(san)
108        board.applyMove(move)
109    return sanmoves
110
111################################################################################
112# listToMoves                                                                  #
113################################################################################
114
115
116def listToMoves(board,
117                movstrs,
118                type=None,
119                testvalidate=False,
120                ignoreErrors=False):
121    # Work on a copy to ensure we don't break things
122    board = board.clone()
123    moves = []
124
125    for mstr in movstrs:
126        try:
127            if type is None:
128                move = parseAny(board, mstr)
129            elif type == SAN:
130                move = parseSAN(board, mstr)
131            elif type == AN:
132                move = parseAN(board, mstr)
133            elif type == LAN:
134                move = parseLAN(board, mstr)
135        except ParsingError:
136            if ignoreErrors:
137                break
138            raise
139
140        if testvalidate and mstr != "--":
141            if not validateMove(board, move):
142                if not ignoreErrors:
143                    raise ParsingError(mstr, 'Validation', board.asFen())
144                break
145
146        moves.append(move)
147        board.applyMove(move)
148
149    return moves
150
151################################################################################
152# toSan                                                                        #
153################################################################################
154
155
156def toSAN(board, move, localRepr=False):
157    """ Returns a Short/Abbreviated Algebraic Notation string of a move
158        The board should be prior to the move """
159
160    def check_or_mate():
161        board_clone = board.clone()
162        board_clone.applyMove(move)
163        sign = ""
164        if board_clone.isChecked():
165            for altmove in genAllMoves(board_clone):
166                if board.variant == ATOMICCHESS:
167                    from pychess.Variants.atomic import kingExplode
168                    if kingExplode(board_clone, altmove, 1 - board_clone.color) and \
169                            not kingExplode(board_clone, altmove, board_clone.color):
170                        sign = "+"
171                        break
172                    elif kingExplode(board_clone, altmove, board_clone.color):
173                        continue
174                board_clone.applyMove(altmove)
175                if board_clone.opIsChecked():
176                    board_clone.popMove()
177                    continue
178                sign = "+"
179                break
180            else:
181                sign = "#"
182        return sign
183
184    flag = move >> 12
185
186    if flag == NULL_MOVE:
187        return "--"
188
189    fcord = (move >> 6) & 63
190    if flag == KING_CASTLE:
191        return "O-O%s" % check_or_mate()
192    elif flag == QUEEN_CASTLE:
193        return "O-O-O%s" % check_or_mate()
194
195    tcord = move & 63
196
197    fpiece = fcord if flag == DROP else board.arBoard[fcord]
198    tpiece = board.arBoard[tcord]
199
200    part0 = ""
201    part1 = ""
202
203    if fpiece != PAWN or flag == DROP:
204        if board.variant in (CAMBODIANCHESS, MAKRUKCHESS):
205            part0 += reprSignMakruk[fpiece]
206        elif board.variant == SITTUYINCHESS:
207            part0 += reprSignSittuyin[fpiece]
208        elif localRepr:
209            part0 += localReprSign[fpiece]
210        else:
211            part0 += reprSign[fpiece]
212
213    part1 = reprCord[tcord]
214
215    if flag == DROP:
216        return "%s@%s%s" % (part0, part1, check_or_mate())
217
218    if fpiece != PAWN:
219        xs = []
220        ys = []
221
222        board_clone = board.clone()
223        for altmove in genAllMoves(board_clone, drops=False):
224            mfcord = FCORD(altmove)
225            if board_clone.arBoard[mfcord] == fpiece and \
226                    mfcord != fcord and \
227                    TCORD(altmove) == tcord:
228                board_clone.applyMove(altmove)
229                if not board_clone.opIsChecked():
230                    xs.append(FILE(mfcord))
231                    ys.append(RANK(mfcord))
232                board_clone.popMove()
233
234        x = FILE(fcord)
235        y = RANK(fcord)
236
237        if ys or xs:
238            if y in ys and x not in xs:
239                # If we share rank with another piece, but not file
240                part0 += reprFile[x]
241            elif x in xs and y not in ys:
242                # If we share file with another piece, but not rank
243                part0 += reprRank[y]
244            elif x in xs and y in ys:
245                # If we share both file and rank with other pieces
246                part0 += reprFile[x] + reprRank[y]
247            else:
248                # If we doesn't share anything, it is standard to put file
249                part0 += reprFile[x]
250
251    if tpiece != EMPTY or flag == ENPASSANT:
252        if not (board.variant == SITTUYINCHESS and fcord == tcord):
253            part1 = "x" + part1
254            if fpiece == PAWN:
255                part0 += reprFile[FILE(fcord)]
256
257    if board.variant == SITTUYINCHESS and flag in PROMOTIONS:
258        part0 = reprCord[fcord]
259
260    notat = part0 + part1
261    if flag in PROMOTIONS:
262        if board.variant in (CAMBODIANCHESS, MAKRUKCHESS):
263            notat += "=" + reprSignMakruk[PROMOTE_PIECE(flag)]
264        elif board.variant == SITTUYINCHESS:
265            notat += "=" + reprSignSittuyin[PROMOTE_PIECE(flag)]
266        elif localRepr:
267            notat += "=" + localReprSign[PROMOTE_PIECE(flag)]
268        else:
269            notat += "=" + reprSign[PROMOTE_PIECE(flag)]
270
271    return "%s%s" % (notat, check_or_mate())
272
273################################################################################
274# parseSan                                                                     #
275################################################################################
276
277
278def parseSAN(board, san):
279    """ Parse a Short/Abbreviated Algebraic Notation string """
280    notat = san
281
282    color = board.color
283
284    if notat == "--":
285        return newMove(board.kings[color], board.kings[color], NULL_MOVE)
286
287    if notat[-1] in "+#":
288        notat = notat[:-1]
289        # If '++' was used in place of #
290        if notat[-1] == "+":
291            notat = notat[:-1]
292
293    flag = NORMAL_MOVE
294
295    # If last char is a piece char, we assue it the promote char
296    c = notat[-1]
297    if c in "KQRBNSMFkqrbnsmf.":
298        c = c.lower()
299        if c == "k" and board.variant != SUICIDECHESS and board.variant != GIVEAWAYCHESS:
300            raise ParsingError(san, _("invalid promoted piece"), board.asFen())
301        elif c == ".":
302            if board.variant in (CAMBODIANCHESS, MAKRUKCHESS, SITTUYINCHESS):
303                # temporary hack for xboard bug
304                flag = QUEEN_PROMOTION
305            else:
306                raise ParsingError(san, "invalid san", board.asFen())
307        else:
308            flag = chr2Sign[c] + 2
309
310        if notat[-2] == "=":
311            notat = notat[:-2]
312        else:
313            notat = notat[:-1]
314
315    if len(notat) < 2:
316        raise ParsingError(san, _("the move needs a piece and a cord"),
317                           board.asFen())
318
319    if notat[0] in "O0o":
320        fcord = board.ini_kings[color]
321        flag = KING_CASTLE if notat in ("O-O", "0-0", "o-o", "OO", "00", "oo") else QUEEN_CASTLE
322        side = flag - QUEEN_CASTLE
323        if FILE(fcord) == 3 and board.variant in (WILDCASTLECHESS,
324                                                  WILDCASTLESHUFFLECHESS):
325            side = 0 if side == 1 else 1
326        if board.variant == FISCHERRANDOMCHESS:
327            tcord = board.ini_rooks[color][side]
328        else:
329            tcord = board.fin_kings[color][side]
330        return newMove(fcord, tcord, flag)
331
332    # LAN is not allowed in pgn spec, but sometimes it occures
333    if "-" in notat:
334        notat = notat.replace("-", "")
335
336    if "@" in notat:
337        tcord = cordDic[notat[-2:]]
338        if notat[0].islower():
339            # Sjeng-ism
340            piece = chr2Sign[notat[0]]
341        else:
342            piece = chrU2Sign[notat[0]]
343        return newMove(piece, tcord, DROP)
344
345    # standard piece letters
346    if notat[0] in "QRBKNSMF":
347        piece = chrU2Sign[notat[0]]
348        notat = notat[1:]
349    # unambigious lowercase piece letters
350    elif notat[0] in "qrknsm":
351        piece = chr2Sign[notat[0]]
352        notat = notat[1:]
353    # a lowercase bishop letter or a pawn capture
354    elif notat[0] == "b" and len(notat) > 2 and board.variant == NORMALCHESS:
355        tcord = cordDic[notat[-2:]]
356        trank = int(notat[-1])
357        # if from and to lines are not neighbours -> Bishop
358        if abs(ord(notat[0]) - ord(notat[-2])) > 1:
359            piece = chr2Sign[notat[0]]
360            notat = notat[1:]
361        # if from and to lines are neighbours (or the same) but to is an empty square
362        # which can't be en-passant square target -> Bishop
363        elif board.arBoard[tcord] == EMPTY and ((color == BLACK and trank != 3) or (color == WHITE and trank != 6)):
364            piece = chr2Sign[notat[0]]
365            notat = notat[1:]
366        # elif "ba3", "bc3" ,"ba6", "bc6"
367        # these can be Bishop or Pawn moves, but we don't try to introspect them (sorry)
368        else:
369            piece = PAWN
370    else:
371        piece = PAWN
372        if notat[-1] in "18" and flag == NORMAL_MOVE and board.variant != SITTUYINCHESS:
373            flag = QUEEN_PROMOTION
374
375    if "x" in notat:
376        notat, tcord = notat.split("x")
377        if tcord not in cordDic:
378            raise ParsingError(san, _("the captured cord (%s) is incorrect") %
379                               tcord, board.asFen())
380
381        tcord = cordDic[tcord]
382
383        if piece == PAWN:
384            # If a pawn is attacking an empty cord, we assue it an enpassant
385            if board.arBoard[tcord] == EMPTY:
386                if (color == BLACK and 2 * 8 <= tcord < 3 * 8) or (color == WHITE and 5 * 8 <= tcord < 6 * 8):
387                    flag = ENPASSANT
388                else:
389                    raise ParsingError(
390                        san, _("pawn capture without target piece is invalid"),
391                        board.asFen())
392    else:
393        if not notat[-2:] in cordDic:
394            raise ParsingError(san, _("the end cord (%s) is incorrect") %
395                               notat[-2:], board.asFen())
396
397        tcord = cordDic[notat[-2:]]
398        notat = notat[:-2]
399
400    # In suicide promoting to king is valid, so
401    # more than 1 king per side can exist !
402    if board.variant != SUICIDECHESS and board.variant != GIVEAWAYCHESS and piece == KING:
403        return newMove(board.kings[color], tcord, flag)
404
405    # If there is any extra location info, like in the move Bexd1 or Nh3f4 we
406    # want to know
407    frank = None
408    ffile = None
409    if notat and notat[0] in reprRank:
410        frank = int(notat[0]) - 1
411        notat = notat[1:]
412    if notat and notat[0] in reprFile:
413        ffile = ord(notat[0]) - ord("a")
414        notat = notat[1:]
415    if notat and notat[0] in reprRank:
416        frank = int(notat[0]) - 1
417        notat = notat[1:]
418        # we know all we want
419        return newMove(frank * 8 + ffile, tcord, flag)
420
421    if piece == PAWN:
422        if (ffile is not None) and ffile != FILE(tcord):
423            # capture
424            if color == WHITE:
425                fcord = tcord - 7 if ffile > FILE(tcord) else tcord - 9
426            else:
427                fcord = tcord + 7 if ffile < FILE(tcord) else tcord + 9
428        else:
429            if color == WHITE:
430                pawns = board.boards[WHITE][PAWN]
431                # In horde white pawns on first rank may move two squares also
432                if board.variant == HORDECHESS and RANK(tcord) == 2 and not (
433                        pawns & fileBits[FILE(tcord)] & rankBits[1]):
434                    fcord = tcord - 16
435                else:
436                    fcord = tcord - 16 if RANK(tcord) == 3 and not (
437                        pawns & fileBits[FILE(tcord)] & rankBits[2]) else tcord - 8
438            else:
439                pawns = board.boards[BLACK][PAWN]
440                fcord = tcord + 16 if RANK(tcord) == 4 and not (
441                    pawns & fileBits[FILE(tcord)] & rankBits[5]) else tcord + 8
442
443            if board.variant == SITTUYINCHESS and flag == QUEEN_PROMOTION:
444                if pawns & fileBits[FILE(tcord)] & rankBits[RANK(tcord)]:
445                    # in place promotion
446                    return newMove(tcord, tcord, flag)
447                else:
448                    # queen move promotion (fcord have to be the closest cord of promotion zone)
449                    fcord = sittuyin_promotion_fcord(board, tcord)
450                    return newMove(fcord, tcord, flag)
451        return newMove(fcord, tcord, flag)
452    else:
453        if board.pieceCount[color][piece] == 1:
454            # we have only one from this kind if piece, so:
455            fcord = firstBit(board.boards[color][piece])
456            return newMove(fcord, tcord, flag)
457        else:
458            # We find all pieces who could have done it. (If san was legal, there should
459            # never be more than one)
460            moves = genPieceMoves(board, piece, tcord)
461            if len(moves) == 1:
462                return moves.pop()
463            else:
464                for move in moves:
465                    f = FCORD(move)
466                    if frank is not None and frank != RANK(f):
467                        continue
468                    if ffile is not None and ffile != FILE(f):
469                        continue
470                    board_clone = board.clone()
471                    board_clone.applyMove(move)
472                    if board_clone.opIsChecked():
473                        continue
474                    return move
475
476    errstring = _("no %(piece)s is able to move to %(cord)s") % {"piece": reprPiece[piece], "cord": reprCord[tcord]}
477    raise ParsingError(san, errstring, board.asFen())
478
479################################################################################
480# toLan                                                                        #
481################################################################################
482
483
484def toLAN(board, move, localRepr=False):
485    """ Returns a Long/Expanded Algebraic Notation string of a move
486        board should be prior to the move """
487
488    fcord = FCORD(move)
489    tcord = TCORD(move)
490    flag = FLAG(move)
491    fpiece = fcord if flag == DROP else board.arBoard[fcord]
492
493    s = ""
494    if fpiece != PAWN or flag == DROP:
495        if board.variant in (CAMBODIANCHESS, MAKRUKCHESS):
496            s = reprSignMakruk[fpiece]
497        elif board.variant == SITTUYINCHESS:
498            s = reprSignSittuyin[fpiece]
499        elif localRepr:
500            s = localReprSign[fpiece]
501        else:
502            s = reprSign[fpiece]
503
504    if flag == DROP:
505        s += "@"
506    else:
507        s += reprCord[FCORD(move)]
508        if board.arBoard[tcord] == EMPTY:
509            s += "-"
510        else:
511            s += "x"
512
513    s += reprCord[tcord]
514
515    if flag in PROMOTIONS:
516        s += "=" + reprSign[PROMOTE_PIECE(flag)]
517
518    return s
519
520################################################################################
521# parseLan                                                                     #
522################################################################################
523
524
525def parseLAN(board, lan):
526    """ Parse a Long/Expanded Algebraic Notation string """
527
528    # To parse LAN pawn moves like "e2-e4" as SAN moves, we have to remove a few
529    # fields
530    if len(lan) == 5:
531        if "x" in lan:
532            # e4xd5 -> exd5
533            return parseSAN(board, lan[0] + lan[3:])
534        else:
535            # e2-e4 -> e4
536            return parseSAN(board, lan[3:])
537
538    # We want to use the SAN parser for LAN moves like "Nb1-c3" or "Rd3xd7"
539    # The san parser should be able to handle most stuff, as long as we remove
540    # the slash
541    if not lan.upper().startswith("O-O") and not lan.startswith("--"):
542        lan = lan.replace("-", "")
543    return parseSAN(board, lan)
544
545################################################################################
546# toAN                                                                         #
547################################################################################
548
549
550def toAN(board, move, short=False, castleNotation=CASTLE_SAN):
551    """ Returns a Algebraic Notation string of a move
552        board should be prior to the move
553        short -- returns the short variant, e.g. f7f8q rather than f7f8=Q
554    """
555
556    fcord = (move >> 6) & 63
557    tcord = move & 63
558    flag = move >> 12
559
560    if flag in (KING_CASTLE, QUEEN_CASTLE):
561        if castleNotation == CASTLE_SAN:
562            return flag == KING_CASTLE and "O-O" or "O-O-O"
563        elif castleNotation == CASTLE_KR:
564            rooks = board.ini_rooks[board.color]
565            tcord = rooks[flag == KING_CASTLE and 1 or 0]
566        # No treatment needed for CASTLE_KK
567
568    if flag == DROP:
569        if board.variant == SITTUYINCHESS:
570            s = "%s@%s" % (reprSignSittuyin[fcord], reprCord[tcord])
571        else:
572            s = "%s@%s" % (reprSign[fcord], reprCord[tcord])
573    else:
574        s = reprCord[fcord] + reprCord[tcord]
575
576    if flag in PROMOTIONS:
577        if short:
578            if board.variant in (CAMBODIANCHESS, MAKRUKCHESS):
579                s += reprSignMakruk[PROMOTE_PIECE(flag)].lower()
580            elif board.variant == SITTUYINCHESS:
581                s += reprSignSittuyin[PROMOTE_PIECE(flag)].lower()
582            else:
583                s += reprSign[PROMOTE_PIECE(flag)].lower()
584        else:
585            if board.variant in (CAMBODIANCHESS, MAKRUKCHESS):
586                s += "=" + reprSignMakruk[PROMOTE_PIECE(flag)]
587            elif board.variant == SITTUYINCHESS:
588                s += "=" + reprSignSittuyin[PROMOTE_PIECE(flag)]
589            else:
590                s += "=" + reprSign[PROMOTE_PIECE(flag)]
591    return s
592
593################################################################################
594# parseAN                                                                      #
595################################################################################
596
597
598def parseAN(board, an):
599    """ Parse an Algebraic Notation string """
600
601    if not 4 <= len(an) <= 6:
602        raise ParsingError(an, "the move must be 4 or 6 chars long",
603                           board.asFen())
604
605    if "@" in an:
606        tcord = cordDic[an[-2:]]
607        if an[0].islower():
608            # Sjeng-ism
609            piece = chr2Sign[an[0]]
610        else:
611            piece = chrU2Sign[an[0]]
612        return newMove(piece, tcord, DROP)
613
614    try:
615        fcord = cordDic[an[:2]]
616        tcord = cordDic[an[2:4]]
617    except KeyError as e:
618        raise ParsingError(an, "the cord (%s) is incorrect" % e.args[0],
619                           board.asFen())
620
621    flag = NORMAL_MOVE
622
623    if len(an) > 4 and not an[-1] in "QRBNMSFqrbnmsf":
624        if (board.variant != SUICIDECHESS and board.variant != GIVEAWAYCHESS) or \
625            (board.variant == SUICIDECHESS or board.variant == GIVEAWAYCHESS) and not an[
626                -1] in "Kk":
627            raise ParsingError(an, "invalid promoted piece", board.asFen())
628
629    if len(an) == 5:
630        # The a7a8q variant
631        flag = chr2Sign[an[4].lower()] + 2
632    elif len(an) == 6:
633        # The a7a8=q variant
634        flag = chr2Sign[an[5].lower()] + 2
635    elif board.arBoard[fcord] == KING:
636        if fcord - tcord == 2:
637            flag = QUEEN_CASTLE
638            if board.variant == FISCHERRANDOMCHESS:
639                tcord = board.ini_rooks[board.color][0]
640        elif fcord - tcord == -2:
641            flag = KING_CASTLE
642            if board.variant == FISCHERRANDOMCHESS:
643                tcord = board.ini_rooks[board.color][1]
644        elif board.arBoard[
645                tcord] == ROOK:
646            color = board.color
647            friends = board.friends[color]
648            if bitPosArray[tcord] & friends:
649                if board.ini_rooks[color][0] == tcord:
650                    flag = QUEEN_CASTLE
651                else:
652                    flag = KING_CASTLE
653        else:
654            flag = NORMAL_MOVE
655    elif board.arBoard[fcord] == PAWN and board.arBoard[tcord] == EMPTY and \
656            FILE(fcord) != FILE(tcord) and RANK(fcord) != RANK(tcord):
657        flag = ENPASSANT
658    elif board.arBoard[fcord] == PAWN:
659        if an[3] in "18" and board.variant != SITTUYINCHESS:
660            flag = QUEEN_PROMOTION
661
662    return newMove(fcord, tcord, flag)
663
664
665################################################################################
666# toFAN                                                                        #
667################################################################################
668
669san2WhiteFanDic = {
670    ord("K"): FAN_PIECES[WHITE][KING],
671    ord("Q"): FAN_PIECES[WHITE][QUEEN],
672    ord("M"): FAN_PIECES[WHITE][QUEEN],
673    ord("F"): FAN_PIECES[WHITE][QUEEN],
674    ord("R"): FAN_PIECES[WHITE][ROOK],
675    ord("B"): FAN_PIECES[WHITE][BISHOP],
676    ord("S"): FAN_PIECES[WHITE][BISHOP],
677    ord("N"): FAN_PIECES[WHITE][KNIGHT],
678    ord("P"): FAN_PIECES[WHITE][PAWN],
679    ord("+"): "†",
680    ord("#"): "‡"
681}
682
683san2BlackFanDic = {
684    ord("K"): FAN_PIECES[BLACK][KING],
685    ord("Q"): FAN_PIECES[BLACK][QUEEN],
686    ord("M"): FAN_PIECES[BLACK][QUEEN],
687    ord("F"): FAN_PIECES[BLACK][QUEEN],
688    ord("R"): FAN_PIECES[BLACK][ROOK],
689    ord("B"): FAN_PIECES[BLACK][BISHOP],
690    ord("S"): FAN_PIECES[BLACK][BISHOP],
691    ord("N"): FAN_PIECES[BLACK][KNIGHT],
692    ord("P"): FAN_PIECES[BLACK][PAWN],
693    ord("+"): "†",
694    ord("#"): "‡"
695}
696
697
698def toFAN(board, move):
699    """ Returns a Figurine Algebraic Notation string of a move """
700
701    san = toSAN(board, move)
702    return san.translate(san2WhiteFanDic)
703
704
705################################################################################
706# parseFAN                                                                     #
707################################################################################
708
709fan2SanDic = {}
710for k, v in san2WhiteFanDic.items():
711    fan2SanDic[ord(v)] = chr(k)
712for k, v in san2BlackFanDic.items():
713    fan2SanDic[ord(v)] = chr(k)
714
715
716def parseFAN(board, fan):
717    """ Parse a Figurine Algebraic Notation string """
718
719    san = fan.translate(fan2SanDic)
720    return parseSAN(board, san)
721
722################################################################################
723# toPolyglot                                                                   #
724################################################################################
725
726
727def toPolyglot(board, move):
728    """ Returns a 16-bit Polyglot-format move
729        board should be prior to the move
730    """
731    pg = move & 4095
732    if FLAG(move) in PROMOTIONS:
733        pg |= (PROMOTE_PIECE(FLAG(move)) - 1) << 12
734    elif FLAG(move) == QUEEN_CASTLE:
735        pg = (pg & 4032) | board.ini_rooks[board.color][0]
736    elif FLAG(move) == KING_CASTLE:
737        pg = (pg & 4032) | board.ini_rooks[board.color][1]
738
739    return pg
740
741################################################################################
742# parsePolyglot                                                                #
743################################################################################
744
745
746def parsePolyglot(board, pg):
747    """ Parse a 16-bit Polyglot-format move """
748
749    tcord = TCORD(pg)
750    fcord = FCORD(pg)
751    flag = NORMAL_MOVE
752    if pg >> 12:
753        flag = FLAG_PIECE((pg >> 12) + 1)
754    elif board.arBoard[fcord] == KING:
755        if board.arBoard[tcord] == ROOK:
756            color = board.color
757            friends = board.friends[color]
758            if bitPosArray[tcord] & friends:
759                if board.ini_rooks[color][0] == tcord:
760                    flag = QUEEN_CASTLE
761                    if board.variant == NORMALCHESS:  # Want e1c1/e8c8
762                        tcord += 2
763                else:
764                    flag = KING_CASTLE
765                    if board.variant == NORMALCHESS:  # Want e1g1/e8g8
766                        tcord -= 1
767    elif board.arBoard[fcord] == PAWN and board.arBoard[tcord] == EMPTY and \
768            FILE(fcord) != FILE(tcord) and RANK(fcord) != RANK(tcord):
769        flag = ENPASSANT
770
771    return newMove(fcord, tcord, flag)
772