1import sys
2from math import e, floor
3from random import randint
4
5from gi.repository import Gtk, GObject
6from gi.repository import Gdk
7
8from pychess.System import uistuff, conf
9from pychess.System.prefix import addDataPrefix
10from pychess.Utils.const import WHITE, DRAW, WHITEWON, BLACKWON
11from pychess.Utils.lutils import leval
12
13__title__ = _("Score")
14__icon__ = addDataPrefix("glade/panel_score.svg")
15__desc__ = _("The score panel tries to evaluate the positions and shows you a graph of the game progress")
16
17
18class Sidepanel:
19    def load(self, gmwidg):
20        self.boardview = gmwidg.board.view
21        self.plot = ScorePlot(self.boardview)
22        self.sw = __widget__ = Gtk.ScrolledWindow()
23        __widget__.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
24        port = Gtk.Viewport()
25        port.add(self.plot)
26        port.set_shadow_type(Gtk.ShadowType.NONE)
27        __widget__.add(port)
28        __widget__.show_all()
29
30        self.plot_cid = self.plot.connect("selected", self.plot_selected)
31        self.cid = self.boardview.connect('shownChanged', self.shownChanged)
32        self.model_cids = [
33            self.boardview.model.connect_after("game_changed", self.game_changed),
34            self.boardview.model.connect_after("moves_undone", self.moves_undone),
35            self.boardview.model.connect_after("analysis_changed", self.analysis_changed),
36            self.boardview.model.connect_after("game_started", self.game_started),
37            self.boardview.model.connect_after("game_terminated", self.on_game_terminated),
38        ]
39
40        def cb_config_changed(none):
41            self.fetch_chess_conf()
42            self.plot.redraw()
43        self.cids_conf = [
44            conf.notify_add("scoreLinearScale", cb_config_changed)
45        ]
46        self.fetch_chess_conf()
47
48        uistuff.keepDown(__widget__)
49
50        return __widget__
51
52    def fetch_chess_conf(self):
53        self.plot.linear_scale = conf.get("scoreLinearScale")
54
55    def on_game_terminated(self, model):
56        self.plot.disconnect(self.plot_cid)
57        self.boardview.disconnect(self.cid)
58        for cid in self.model_cids:
59            self.boardview.model.disconnect(cid)
60        for cid in self.cids_conf:
61            conf.notify_remove(cid)
62
63    def moves_undone(self, model, moves):
64        for i in range(moves):
65            self.plot.undo()
66
67        # As shownChanged will normally be emitted just after game_changed -
68        # if we are viewing the latest position - we can do the selection change
69        # now, and thereby avoid redraw being called twice
70        if self.plot.selected == model.ply - model.lowply:
71            self.plot.select(model.ply - model.lowply - moves)
72        self.plot.redraw()
73
74    def game_changed(self, model, ply):
75        if len(self.plot) + model.lowply > ply:
76            return
77
78        for i in range(len(self.plot) + model.lowply, ply):
79            if i in model.scores:
80                points = model.scores[i][1]
81                points = points * -1 if i % 2 == 1 else points
82            else:
83                points = leval.evaluateComplete(
84                    model.getBoardAtPly(i).board, WHITE)
85            self.plot.addScore(points)
86
87        if model.status == DRAW:
88            points = 0
89        elif model.status == WHITEWON:
90            points = sys.maxsize
91        elif model.status == BLACKWON:
92            points = -sys.maxsize
93        else:
94            if ply in model.scores:
95                points = model.scores[ply][1]
96                points = points * -1 if ply % 2 == 1 else points
97            else:
98                try:
99                    points = leval.evaluateComplete(
100                        model.getBoardAtPly(ply).board, WHITE)
101                except IndexError:
102                    return
103        self.plot.addScore(points)
104
105        # As shownChanged will normally be emitted just after game_changed -
106        # if we are viewing the latest position - we can do the selection change
107        # now, and thereby avoid redraw being called twice
108        if self.plot.selected == ply - model.lowply - 1:
109            self.plot.select(ply - model.lowply)
110        self.plot.redraw()
111
112        # Uncomment this to debug eval function
113        # ---
114        # board = model.boards[-1].board
115        # opboard = model.boards[-1].clone().board
116        # opboard.setColor(1 - opboard.color)
117        # material, phase = leval.evalMaterial(board)
118        # if board.color == WHITE:
119        #     print("material", -material)
120        #     e1 = leval.evalKingTropism(board)
121        #     e2 = leval.evalKingTropism(opboard)
122        #     print("evaluation: %d + %d = %d " % (e1, e2, e1 + e2))
123        #     p1 = leval.evalPawnStructure(board, phase)
124        #     p2 = leval.evalPawnStructure(opboard, phase)
125        #     print("pawns: %d + %d = %d " % (p1, p2, p1 + p2))
126        #     print("knights:", -leval.evalKnights(board))
127        #     print("king:", -leval.evalKing(board, phase))
128        # else:
129        #     print("material", material)
130        #     print("evaluation:", leval.evalKingTropism(board))
131        #     print("pawns:", leval.evalPawnStructure(board, phase))
132        #     print("pawns2:", leval.evalPawnStructure(opboard, phase))
133        #     print("pawns3:", leval.evalPawnStructure(board, phase) +
134        #           leval.evalPawnStructure(opboard, phase))
135        #     print("knights:", leval.evalKnights(board))
136        #     print("king:", leval.evalKing(board, phase))
137        # print("----------------------")
138
139    def game_started(self, model):
140        if model.lesson_game:
141            return
142
143        self.game_changed(model, model.ply)
144
145    def shownChanged(self, boardview, shown):
146        if not boardview.shownIsMainLine():
147            return
148        if self.plot.selected != shown:
149            self.plot.select(shown - self.boardview.model.lowply)
150            self.plot.redraw()
151
152    def analysis_changed(self, gamemodel, ply):
153        if self.boardview.animating:
154            return
155
156        if not self.boardview.shownIsMainLine():
157            return
158        if ply - gamemodel.lowply > len(self.plot.scores) - 1:
159            # analysis line of yet undone position
160            return
161
162        color = (ply - 1) % 2
163        score = gamemodel.scores[ply][1]
164        score = score * -1 if color == WHITE else score
165        self.plot.changeScore(ply - gamemodel.lowply, score)
166        self.plot.redraw()
167
168    def plot_selected(self, plot, selected):
169        try:
170            board = self.boardview.model.boards[selected]
171        except IndexError:
172            return
173        self.boardview.setShownBoard(board)
174
175
176class ScorePlot(Gtk.DrawingArea):
177
178    __gtype_name__ = "ScorePlot" + str(randint(0, sys.maxsize))
179    __gsignals__ = {"selected": (GObject.SignalFlags.RUN_FIRST, None, (int, ))}
180
181    def __init__(self, boardview):
182        GObject.GObject.__init__(self)
183        self.boardview = boardview
184        self.connect("draw", self.expose)
185        self.connect("button-press-event", self.press)
186        self.props.can_focus = True
187        self.set_events(Gdk.EventMask.BUTTON_PRESS_MASK |
188                        Gdk.EventMask.KEY_PRESS_MASK)
189        self.scores = []
190        self.selected = 0
191
192    def get_move_height(self):
193        c = self.__len__()
194        w = self.get_allocation().width
195        if c != 0:
196            w = int(floor(w / c))
197        return max(min(w, 24), 1)
198
199    def addScore(self, score):
200        self.scores.append(score)
201
202    def changeScore(self, ply, score):
203        if self.scores:
204            self.scores[ply] = score
205
206    def __len__(self):
207        return len(self.scores)
208
209    def undo(self):
210        del self.scores[-1]
211
212    def select(self, index):
213        self.selected = index
214
215    def clear(self):
216        del self.scores[:]
217
218    def redraw(self):
219        if self.get_window():
220            a = self.get_allocation()
221            rect = Gdk.Rectangle()
222            rect.x, rect.y, rect.width, rect.height = (0, 0, a.width, a.height)
223            self.get_window().invalidate_rect(rect, True)
224            self.get_window().process_updates(True)
225
226    def press(self, widget, event):
227        self.grab_focus()
228        self.emit('selected', event.x / self.get_move_height())
229
230    def expose(self, widget, context):
231        a = widget.get_allocation()
232        context.rectangle(0, 0, a.width, a.height)
233        context.clip()
234        self.draw(context)
235        return False
236
237    def draw(self, cr):
238        m = self.boardview.model
239        if m.isPlayingICSGame():
240            return
241
242        width = self.get_allocation().width
243        height = self.get_allocation().height
244
245        ########################################
246        # Draw background                      #
247        ########################################
248
249        cr.set_source_rgb(1, 1, 1)
250        cr.rectangle(0, 0, width, height)
251        cr.fill()
252
253        ########################################
254        # Draw the actual plot (dark area)     #
255        ########################################
256
257        def sign(n):
258            return n == 0 and 1 or n / abs(n)
259
260        def mapper(score):
261            if self.linear_scale:
262                return min(abs(score), 800) / 800 * sign(score)  # Linear
263            else:
264                return (e ** (5e-4 * abs(score)) - 1) * sign(score)  # Exponentially stretched
265
266        if self.scores:
267            cr.set_source_rgb(0, 0, 0)
268            cr.move_to(0, height)
269            cr.line_to(0, (height / 2.) * (1 + mapper(self.scores[0])))
270            for i, score in enumerate(self.scores):
271                x = (i + 1) * self.get_move_height()
272                y = (height / 2.) * (1 + mapper(score))
273                y = max(0, min(height, y))
274                cr.line_to(x, y)
275            cr.line_to(x, height)
276            cr.fill()
277        else:
278            x = 0
279        cr.set_source_rgb(0.9, 0.9, 0.9)
280        cr.rectangle(x, 0, width, height)
281        cr.fill()
282
283        ########################################
284        # Draw middle line and markers         #
285        ########################################
286
287        cr.set_line_width(0.25)
288        markers = [16, -16, 8, -8, 3, -3, 0]  # centipawns
289        for mark in markers:
290            if mark == 0:
291                cr.set_source_rgb(1, 0, 0)
292            else:
293                cr.set_source_rgb(0.85, 0.85, 0.85)
294            y = (height / 2.) * (1 + mapper(100 * mark))
295            y = max(0, min(height, y))
296            cr.move_to(0, y)
297            cr.line_to(width, y)
298            cr.stroke()
299
300        ########################################
301        # Draw selection                       #
302        ########################################
303
304        lw = 2
305        cr.set_line_width(lw)
306        s = self.get_move_height()
307        x = self.selected * s
308        cr.rectangle(x - lw / 2, lw / 2, s + lw, height - lw)
309        found, color = self.get_style_context().lookup_color("p_bg_selected")
310        cr.set_source_rgba(color.red, color.green, color.blue, .15)
311        cr.fill_preserve()
312        cr.set_source_rgb(color.red, color.green, color.blue)
313        cr.stroke()
314