1import random
2from io import StringIO
3
4from gi.repository import Gtk
5
6from pychess.compat import create_task
7from pychess.System.prefix import addDataPrefix
8from pychess.Utils.const import WHITE, BLACK, LOCAL, NORMALCHESS, ARTIFICIAL, chr2Sign, chrU2Sign, FAN_PIECES, HINT, ENDGAME
9from pychess.Utils.LearnModel import LearnModel
10from pychess.Utils.TimeModel import TimeModel
11from pychess.Utils.lutils.attack import isAttacked
12from pychess.Utils.lutils.LBoard import LBoard
13from pychess.Utils.lutils.lmove import FILE, RANK
14from pychess.Variants import variants
15from pychess.Players.Human import Human
16from pychess.Players.engineNest import discoverer
17from pychess.System import conf
18from pychess.perspectives import perspective_manager
19from pychess.Savers import fen as fen_loader
20
21__title__ = _("Endgames")
22
23__icon__ = addDataPrefix("glade/panel_book.svg")
24
25__desc__ = _("Practice endgames with computer")
26
27
28# TODO: get it from a text file
29ENDGAMES = (
30    ("kpk", "King and Pawn vs King"),
31    ("kbnk", "King, Bishop and Knight vs King"),
32    ("kbbk", "King and 2 Bishops vs King"),
33    ("krk", "King and Rook vs King"),
34    ("kqk", "King and Queen vs King"),
35    ("kqkr", "King and Queen vs King and Rook"),
36    ("krpkr", "King, Rook and Pawn vs King and Rook"),
37    ("kppkp", "King and 2 Pawns vs King and Pawn"),
38    ("kpkp", "King and Pawn vs King and Pawn"),
39    ("kqpkq", "King, Queen and Pawn vs King and Queen"),
40    ("knnkp", "King and Two Knights and vs King and Pawn"),
41    ("kppkpp", "King and two pawns vs King and two pawns"),
42    ("kqqkqr", "King and two queens vs King and Queen"),
43)
44
45
46class Sidepanel():
47    def load(self, persp):
48        self.persp = persp
49        self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
50
51        self.tv = Gtk.TreeView()
52
53        renderer = Gtk.CellRendererText()
54        renderer.props.font = "Times 14"
55        column = Gtk.TreeViewColumn(_("White"), renderer, text=0)
56        self.tv.append_column(column)
57
58        renderer = Gtk.CellRendererText()
59        renderer.props.font = "Times 14"
60        column = Gtk.TreeViewColumn(_("Black"), renderer, text=1)
61        self.tv.append_column(column)
62
63        renderer = Gtk.CellRendererText()
64        column = Gtk.TreeViewColumn(_("Title"), renderer, text=2)
65        self.tv.append_column(column)
66
67        self.tv.connect("row-activated", self.row_activated)
68
69        self.store = Gtk.ListStore(str, str, str)
70
71        for pieces, title in ENDGAMES:
72            if pieces.count("k") != 2:
73                print("Game needs exactly 2 kings! %s" % pieces)
74                continue
75            elif len(pieces) > 6:
76                print("Max 6 pieces, please! %s" % pieces)
77                continue
78            else:
79                for piece in pieces:
80                    if piece not in ("kqrbnp"):
81                        print("Invalid piece %s in %s" % (piece, pieces))
82                        continue
83
84                pos = pieces.rfind("k")
85                white_pieces, black_pieces = pieces[:pos], pieces[pos:]
86                wfan = []
87                for piece in white_pieces:
88                    wfan.append(FAN_PIECES[0][chr2Sign[piece]])
89                bfan = []
90                for piece in black_pieces:
91                    bfan.append(FAN_PIECES[1][chr2Sign[piece]])
92                self.store.append(["".join(wfan), "".join(bfan), title])
93
94        self.tv.set_model(self.store)
95        self.tv.get_selection().set_mode(Gtk.SelectionMode.BROWSE)
96        self.tv.set_cursor(conf.get("learncombo%s" % ENDGAME))
97
98        scrollwin = Gtk.ScrolledWindow()
99        scrollwin.add(self.tv)
100        scrollwin.show_all()
101
102        self.box.pack_start(scrollwin, True, True, 0)
103        self.box.show_all()
104
105        return self.box
106
107    def row_activated(self, widget, path, col):
108        if path is None:
109            return
110        else:
111            pieces = ENDGAMES[path[0]][0].lower()
112            conf.set("categorycombo", ENDGAME)
113            from pychess.widgets.TaskerManager import learn_tasker
114            learn_tasker.learn_combo.set_active(path[0])
115            start_endgame_from(pieces)
116
117
118def start_endgame_from(pieces):
119    fen = create_fen(pieces)
120
121    timemodel = TimeModel(0, 0)
122    gamemodel = LearnModel(timemodel)
123    gamemodel.set_learn_data(ENDGAME, pieces)
124
125    player_name = conf.get("firstName")
126    p0 = (LOCAL, Human, (WHITE, player_name), player_name)
127
128    engine = discoverer.getEngineByName(discoverer.getEngineLearn())
129    ponder_off = True
130    engine_name = discoverer.getName(engine)
131    p1 = (ARTIFICIAL, discoverer.initPlayerEngine,
132          (engine, BLACK, 20, variants[NORMALCHESS], 60, 0, 0, ponder_off), engine_name)
133
134    def restart_analyzer(gamemodel):
135        create_task(gamemodel.restart_analyzer(HINT))
136    gamemodel.connect("learn_success", restart_analyzer)
137
138    def on_game_started(gamemodel):
139        perspective.activate_panel("annotationPanel")
140        create_task(gamemodel.start_analyzer(HINT, force_engine=discoverer.getEngineLearn()))
141    gamemodel.connect("game_started", on_game_started)
142
143    perspective = perspective_manager.get_perspective("games")
144    create_task(perspective.generalStart(gamemodel, p0, p1, loaddata=(StringIO(fen), fen_loader, 0, -1)))
145
146
147def create_fen(pieces):
148    """ Create a random FEN position using given pieces """
149
150    pos = pieces.rfind("k")
151    pieces = pieces[:pos], pieces[pos:]
152
153    ok = False
154    while not ok:
155        lboard = LBoard()
156        lboard.applyFen("8/8/8/8/8/8/8/8 w - - 0 1")
157        bishop_cords = [[], []]
158        bishop_colors_ok = True
159
160        cords = list(range(0, 64))
161        pawn_cords = list(range(0 + 8, 64 - 8))
162
163        # Order of color is important here to prevent offering
164        # positions with trivial captures in first move
165        for color in (WHITE, BLACK):
166            for char in pieces[color]:
167                piece = chrU2Sign[char.upper()]
168                attacked = True
169                limit = 100
170                while attacked and limit > 0:
171                    cord = random.choice(pawn_cords if char == "p" else cords)
172                    attacked = isAttacked(lboard, cord, 1 - color)
173                    limit -= 1
174                lboard._addPiece(cord, piece, color)
175                cords.remove(cord)
176                if cord in pawn_cords:
177                    pawn_cords.remove(cord)
178                if char == "b":
179                    bishop_cords[color].append(cord)
180
181            # 2 same color bishop is not ok
182            if len(bishop_cords[color]) == 2 and bishop_colors_ok:
183                b0, b1 = bishop_cords[color]
184                b0_color = BLACK if RANK(b0) % 2 == FILE(b0) % 2 else WHITE
185                b1_color = BLACK if RANK(b1) % 2 == FILE(b1) % 2 else WHITE
186                if b0_color == b1_color:
187                    bishop_colors_ok = False
188                    break
189
190        ok = (not lboard.isChecked()) and (not lboard.opIsChecked()) and bishop_colors_ok
191
192    fen = lboard.asFen()
193    return fen
194