1# -*- coding: UTF-8 -*-
2
3
4from gi.repository import Gtk, Gdk, GObject
5
6from pychess.System import conf
7from pychess.Utils.Cord import Cord
8from pychess.Utils.Move import Move, parseAny, toAN
9from pychess.Utils.const import ARTIFICIAL, FLAG_CALL, ABORT_OFFER, LOCAL, TAKEBACK_OFFER, \
10    ADJOURN_OFFER, DRAW_OFFER, RESIGNATION, HURRY_ACTION, PAUSE_OFFER, RESUME_OFFER, RUNNING, \
11    DROP, DROP_VARIANTS, PAWN, QUEEN, SITTUYINCHESS, QUEEN_PROMOTION
12
13from pychess.Utils.logic import validate
14from pychess.Utils.lutils import lmove, lmovegen
15from pychess.Utils.lutils.lmove import ParsingError
16
17from . import preferencesDialog
18from .PromotionDialog import PromotionDialog
19from .BoardView import BoardView, rect, join
20
21
22class BoardControl(Gtk.EventBox):
23    """ Creates a BoardView for GameModel to control move selection,
24        action menu selection and emits signals to let Human player
25        make moves and emit offers.
26        SetuPositionDialog uses setup_position=True to disable most validation.
27        When game_preview=True just do circles and arrows
28    """
29
30    __gsignals__ = {
31        'shapes_changed': (GObject.SignalFlags.RUN_FIRST, None, ()),
32        'piece_moved': (GObject.SignalFlags.RUN_FIRST, None, (object, int)),
33        'action': (GObject.SignalFlags.RUN_FIRST, None, (str, object, object))
34    }
35
36    def __init__(self, gamemodel, action_menu_items, setup_position=False, game_preview=False):
37        GObject.GObject.__init__(self)
38        self.setup_position = setup_position
39        self.game_preview = game_preview
40
41        self.view = BoardView(gamemodel, setup_position=setup_position)
42
43        self.add(self.view)
44        self.variant = gamemodel.variant
45        self.promotionDialog = PromotionDialog(self.variant.variant)
46
47        self.RANKS = gamemodel.boards[0].RANKS
48        self.FILES = gamemodel.boards[0].FILES
49
50        self.action_menu_items = action_menu_items
51        self.connections = {}
52        for key, menuitem in self.action_menu_items.items():
53            if menuitem is None:
54                print(key)
55            # print("...connect to", key, menuitem)
56            self.connections[menuitem] = menuitem.connect(
57                "activate", self.actionActivate, key)
58        self.view_cid = self.view.connect("shownChanged", self.shownChanged)
59
60        self.gamemodel = gamemodel
61        self.gamemodel_cids = []
62        self.gamemodel_cids.append(gamemodel.connect("moves_undoing", self.moves_undone))
63        self.gamemodel_cids.append(gamemodel.connect("game_ended", self.game_ended))
64        self.gamemodel_cids.append(gamemodel.connect("game_started", self.game_started))
65
66        self.cids = []
67        self.cids.append(self.connect("button_press_event", self.button_press))
68        self.cids.append(self.connect("button_release_event", self.button_release))
69        self.add_events(Gdk.EventMask.LEAVE_NOTIFY_MASK |
70                        Gdk.EventMask.POINTER_MOTION_MASK)
71        self.cids.append(self.connect("motion_notify_event", self.motion_notify))
72        self.cids.append(self.connect("leave_notify_event", self.leave_notify))
73
74        self.selected_last = None
75        self.normalState = NormalState(self)
76        self.selectedState = SelectedState(self)
77        self.activeState = ActiveState(self)
78        self.lockedNormalState = LockedNormalState(self)
79        self.lockedSelectedState = LockedSelectedState(self)
80        self.lockedActiveState = LockedActiveState(self)
81        self.currentState = self.normalState
82
83        self.lockedPly = self.view.shown
84        self.possibleBoards = {
85            self.lockedPly: self._genPossibleBoards(self.lockedPly)
86        }
87
88        self.allowPremove = False
89
90        def onGameStart(gamemodel):
91            if not self.setup_position:
92                for player in gamemodel.players:
93                    if player.__type__ == LOCAL:
94                        self.allowPremove = True
95
96        self.gamemodel_cids.append(gamemodel.connect("game_started", onGameStart))
97        self.keybuffer = ""
98
99        self.pre_arrow_from = None
100        self.pre_arrow_to = None
101
102    def _del(self):
103        self.view.disconnect(self.view_cid)
104        for cid in self.cids:
105            self.disconnect(cid)
106
107        for obj, conid in self.connections.items():
108            # print("...disconnect from ", obj)
109            obj.disconnect(conid)
110        self.connections = {}
111        self.action_menu_items = {}
112
113        for cid in self.gamemodel_cids:
114            self.gamemodel.disconnect(cid)
115
116        self.view._del()
117
118        self.promotionDialog = None
119
120        self.normalState = None
121        self.selectedState = None
122        self.activeState = None
123        self.lockedNormalState = None
124        self.lockedSelectedState = None
125        self.lockedActiveState = None
126        self.currentState = None
127
128    def getPromotion(self):
129        color = self.view.model.boards[-1].color
130        variant = self.view.model.boards[-1].variant
131        promotion = self.promotionDialog.runAndHide(color, variant)
132        return promotion
133
134    def play_sound(self, move, board):
135        if move.is_capture(board):
136            sound = "aPlayerCaptures"
137        else:
138            sound = "aPlayerMoves"
139
140        if board.board.isChecked():
141            sound = "aPlayerChecks"
142
143        preferencesDialog.SoundTab.playAction(sound)
144
145    def play_or_add_move(self, board, move):
146        if board.board.next is None:
147            # at the end of variation or main line
148            if not self.view.shownIsMainLine():
149                # add move to existing variation
150                self.view.model.add_move2variation(board, move, self.view.shown_variation_idx)
151                self.view.showNext()
152            else:
153                # create new variation
154                new_vari = self.view.model.add_variation(board, [move])
155                self.view.setShownBoard(new_vari[-1])
156        else:
157            # inside variation or main line
158            if board.board.next.lastMove == move.move:
159                # replay mainline move
160                if self.view.model.lesson_game:
161                    next_board = self.view.model.getBoardAtPly(self.view.shown + 1, self.view.shown_variation_idx)
162                    self.play_sound(move, board)
163                    incr = 1 if len(self.view.model.variations[self.view.shown_variation_idx]) - 1 == board.ply - self.view.model.lowply + 1 else 2
164                    if incr == 2:
165                        next_next_board = self.view.model.getBoardAtPly(self.view.shown + 2, self.view.shown_variation_idx)
166                        # If there is any opponent move variation let the user choose opp next move
167                        if any(child for child in next_next_board.board.children if isinstance(child, list)):
168                            self.view.infobar.opp_turn()
169                            self.view.showNext()
170                        # If there is some comment to read let the user read it before opp move
171                        elif any(child for child in next_board.board.children if isinstance(child, str)):
172                            self.view.infobar.opp_turn()
173                            self.view.showNext()
174
175                        # If there is nothing to wait for we make opp next move
176                        else:
177                            self.view.showNext()
178                            self.view.infobar.your_turn()
179                            self.view.showNext()
180                    else:
181                        if self.view.shownIsMainLine():
182                            preferencesDialog.SoundTab.playAction("puzzleSuccess")
183                            self.view.infobar.get_next_puzzle()
184                            self.view.model.emit("learn_success")
185                            self.view.showNext()
186                        else:
187                            self.view.infobar.back_to_mainline()
188                            self.view.showNext()
189                else:
190                    self.view.showNext()
191
192            elif board.board.next.children:
193                if self.view.model.lesson_game:
194                    self.play_sound(move, board)
195                    self.view.infobar.retry()
196
197                # try to find this move in variations
198                for i, vari in enumerate(board.board.next.children):
199                    for node in vari:
200                        if type(node) != str and node.lastMove == move.move and node.plyCount == board.ply + 1:
201                            # replay variation move
202                            self.view.setShownBoard(node.pieceBoard)
203                            return
204
205                # create new variation
206                new_vari = self.view.model.add_variation(board, [move])
207                self.view.setShownBoard(new_vari[-1])
208
209            else:
210                if self.view.model.lesson_game:
211                    self.play_sound(move, board)
212                    self.view.infobar.retry()
213
214                # create new variation
215                new_vari = self.view.model.add_variation(board, [move])
216                self.view.setShownBoard(new_vari[-1])
217
218    def emit_move_signal(self, cord0, cord1, promotion=None):
219        # Game end can change cord0 to None while dragging a piece
220        if cord0 is None:
221            return
222        board = self.getBoard()
223        color = board.color
224        # Ask player for which piece to promote into. If this move does not
225        # include a promotion, QUEEN will be sent as a dummy value, but not used
226        if promotion is None and board[cord0].sign == PAWN and \
227                cord1.cord in board.PROMOTION_ZONE[color] and \
228                self.variant.variant != SITTUYINCHESS:
229            if len(self.variant.PROMOTIONS) == 1:
230                promotion = lmove.PROMOTE_PIECE(self.variant.PROMOTIONS[0])
231            else:
232                if conf.get("autoPromote"):
233                    promotion = lmove.PROMOTE_PIECE(QUEEN_PROMOTION)
234                else:
235                    promotion = self.getPromotion()
236                    if promotion is None:
237                        # Put back pawn moved be d'n'd
238                        self.view.runAnimation(redraw_misc=False)
239                        return
240        if promotion is None and board[cord0].sign == PAWN and \
241                cord0.cord in board.PROMOTION_ZONE[color] and \
242                self.variant.variant == SITTUYINCHESS:
243            # no promotion allowed if we have queen
244            if board.board.boards[color][QUEEN]:
245                promotion = None
246            # in place promotion
247            elif cord1.cord in board.PROMOTION_ZONE[color]:
248                promotion = lmove.PROMOTE_PIECE(QUEEN_PROMOTION)
249            # queen move promotion (but not a pawn capture!)
250            elif board[cord1] is None and (cord0.cord + cord1.cord) % 2 == 1:
251                promotion = lmove.PROMOTE_PIECE(QUEEN_PROMOTION)
252
253        if cord0.x < 0 or cord0.x > self.FILES - 1:
254            move = Move(lmovegen.newMove(board[cord0].piece, cord1.cord, DROP))
255        else:
256            move = Move(cord0, cord1, board, promotion)
257
258        if (self.view.model.curplayer.__type__ == LOCAL or self.view.model.examined) and \
259                self.view.shownIsMainLine() and \
260                self.view.model.boards[-1] == board and \
261                self.view.model.status == RUNNING:
262            # emit move
263            if self.setup_position:
264                self.emit("piece_moved", (cord0, cord1), board[cord0].color)
265            else:
266                self.emit("piece_moved", move, color)
267                if self.view.model.examined:
268                    self.view.model.connection.bm.sendMove(toAN(board, move))
269        else:
270            self.play_or_add_move(board, move)
271
272    def actionActivate(self, widget, key):
273        """ Put actions from a menu or similar """
274        curplayer = self.view.model.curplayer
275        if key == "call_flag":
276            self.emit("action", FLAG_CALL, curplayer, None)
277        elif key == "abort":
278            self.emit("action", ABORT_OFFER, curplayer, None)
279        elif key == "adjourn":
280            self.emit("action", ADJOURN_OFFER, curplayer, None)
281        elif key == "draw":
282            self.emit("action", DRAW_OFFER, curplayer, None)
283        elif key == "resign":
284            self.emit("action", RESIGNATION, curplayer, None)
285        elif key == "ask_to_move":
286            self.emit("action", HURRY_ACTION, curplayer, None)
287        elif key == "undo1":
288            board = self.view.model.getBoardAtPly(self.view.shown, variation=self.view.shown_variation_idx)
289            if board.board.next is not None or board.board.children:
290                return
291            if not self.view.shownIsMainLine():
292                self.view.model.undo_in_variation(board)
293                return
294
295            waitingplayer = self.view.model.waitingplayer
296            if curplayer.__type__ == LOCAL and \
297                    (waitingplayer.__type__ == ARTIFICIAL or
298                     self.view.model.isPlayingICSGame()) and \
299                    self.view.model.ply - self.view.model.lowply > 1:
300                self.emit("action", TAKEBACK_OFFER, curplayer, 2)
301            else:
302                self.emit("action", TAKEBACK_OFFER, curplayer, 1)
303        elif key == "pause1":
304            self.emit("action", PAUSE_OFFER, curplayer, None)
305        elif key == "resume1":
306            self.emit("action", RESUME_OFFER, curplayer, None)
307
308    def shownChanged(self, view, shown):
309        if self.view is None:
310            return
311        self.lockedPly = self.view.shown
312        self.possibleBoards[self.lockedPly] = self._genPossibleBoards(
313            self.lockedPly)
314        if self.view.shown - 2 in self.possibleBoards:
315            del self.possibleBoards[self.view.shown - 2]
316
317    def moves_undone(self, gamemodel, moves):
318        self.view.selected = None
319        self.view.active = None
320        self.view.hover = None
321        self.view.dragged_piece = None
322        self.view.setPremove(None, None, None, None)
323        if not self.view.model.examined:
324            self.currentState = self.lockedNormalState
325
326    def game_ended(self, gamemodel, reason):
327        self.selected_last = None
328        self.view.selected = None
329        self.view.active = None
330        self.view.hover = None
331        self.view.dragged_piece = None
332        self.view.setPremove(None, None, None, None)
333        self.currentState = self.normalState
334
335        self.view.startAnimation()
336
337    def game_started(self, gamemodel):
338        if self.view.model.lesson_game:
339            if "FEN" in gamemodel.tags:
340                if gamemodel.orientation != gamemodel.starting_color:
341                    self.view.showNext()
342            else:
343                self.view.infobar.get_next_puzzle()
344                self.view.model.emit("learn_success")
345
346    def getBoard(self):
347        return self.view.model.getBoardAtPly(self.view.shown,
348                                             self.view.shown_variation_idx)
349
350    def isLastPlayed(self, board):
351        return board == self.view.model.boards[-1]
352
353    def setLocked(self, locked):
354        do_animation = False
355
356        if locked and self.isLastPlayed(self.getBoard()) and \
357                self.view.model.status == RUNNING:
358            if self.view.model.status != RUNNING:
359                self.view.selected = None
360                self.view.active = None
361                self.view.hover = None
362                self.view.dragged_piece = None
363                do_animation = True
364
365            if self.currentState == self.selectedState:
366                self.currentState = self.lockedSelectedState
367            elif self.currentState == self.activeState:
368                self.currentState = self.lockedActiveState
369            else:
370                self.currentState = self.lockedNormalState
371        else:
372            if self.currentState == self.lockedSelectedState:
373                self.currentState = self.selectedState
374            elif self.currentState == self.lockedActiveState:
375                self.currentState = self.activeState
376            else:
377                self.currentState = self.normalState
378
379        if do_animation:
380            self.view.startAnimation()
381
382    def setStateSelected(self):
383        if self.currentState in (self.lockedNormalState,
384                                 self.lockedSelectedState,
385                                 self.lockedActiveState):
386            self.currentState = self.lockedSelectedState
387        else:
388            self.view.setPremove(None, None, None, None)
389            self.currentState = self.selectedState
390
391    def setStateActive(self):
392        if self.currentState in (self.lockedNormalState,
393                                 self.lockedSelectedState,
394                                 self.lockedActiveState):
395            self.currentState = self.lockedActiveState
396        else:
397            self.view.setPremove(None, None, None, None)
398            self.currentState = self.activeState
399
400    def setStateNormal(self):
401        if self.currentState in (self.lockedNormalState,
402                                 self.lockedSelectedState,
403                                 self.lockedActiveState):
404            self.currentState = self.lockedNormalState
405        else:
406            self.view.setPremove(None, None, None, None)
407            self.currentState = self.normalState
408
409    def color(self, event):
410        state = event.get_state()
411        if state & Gdk.ModifierType.SHIFT_MASK and state & Gdk.ModifierType.CONTROL_MASK:
412            return "Y"
413        elif state & Gdk.ModifierType.SHIFT_MASK:
414            return "R"
415        elif state & Gdk.ModifierType.CONTROL_MASK:
416            return "B"
417        else:
418            return "G"
419
420    def button_press(self, widget, event):
421        if event.button == 3:
422            # first we will draw a circle
423            cord = self.currentState.point2Cord(event.x, event.y, self.color(event))
424            if cord is None or cord.x < 0 or cord.x > self.FILES or cord.y < 0 or cord.y > self.RANKS:
425                return
426            self.pre_arrow_from = cord
427            self.view.pre_circle = cord
428            self.view.redrawCanvas()
429            return
430        else:
431            # remove all circles and arrows
432            need_redraw = False
433            if self.view.arrows:
434                self.view.arrows.clear()
435                need_redraw = True
436            if self.view.circles:
437                self.view.circles.clear()
438                need_redraw = True
439            if self.view.pre_arrow is not None:
440                self.view.pre_arrow = None
441                need_redraw = True
442            if self.view.pre_circle is not None:
443                self.view.pre_circle = None
444                need_redraw = True
445            if need_redraw:
446                self.view.redrawCanvas()
447
448        if self.game_preview:
449            return
450        return self.currentState.press(event.x, event.y, event.button)
451
452    def button_release(self, widget, event):
453        if event.button == 3:
454            # remove or finalize circle/arrow as needed
455            cord = self.currentState.point2Cord(event.x, event.y, self.color(event))
456            if cord is None or cord.x < 0 or cord.x > self.FILES or cord.y < 0 or cord.y > self.RANKS:
457                return
458            if self.view.pre_circle == cord:
459                if cord in self.view.circles:
460                    self.view.circles.remove(cord)
461                else:
462                    self.view.circles.add(cord)
463                self.view.pre_circle = None
464                self.emit("shapes_changed")
465
466            if self.view.pre_arrow is not None:
467                if self.view.pre_arrow in self.view.arrows:
468                    self.view.arrows.remove(self.view.pre_arrow)
469                else:
470                    self.view.arrows.add(self.view.pre_arrow)
471                self.view.pre_arrow = None
472                self.emit("shapes_changed")
473
474            self.pre_arrow_from = None
475            self.pre_arrow_to = None
476            self.view.redrawCanvas()
477            return
478
479        if self.game_preview:
480            return
481        return self.currentState.release(event.x, event.y)
482
483    def motion_notify(self, widget, event):
484        to = self.currentState.point2Cord(event.x, event.y)
485        if to is None or to.x < 0 or to.x > self.FILES or to.y < 0 or to.y > self.RANKS:
486            return
487        if self.pre_arrow_from is not None:
488            if to != self.pre_arrow_from:
489                # this will be an arrow
490                if self.pre_arrow_to is not None and to != self.pre_arrow_to:
491                    # first remove the old one
492                    self.view.pre_arrow = None
493                    self.view.redrawCanvas()
494
495                arrow = self.pre_arrow_from, to
496                if arrow != self.view.pre_arrow:
497                    # draw the new arrow
498                    self.view.pre_arrow = arrow
499                    self.view.pre_circle = None
500                    self.view.redrawCanvas()
501                    self.pre_arrow_to = to
502
503            elif self.view.pre_circle is None:
504                # back to circle
505                self.view.pre_arrow = None
506                self.view.pre_circle = to
507                self.view.redrawCanvas()
508
509        return self.currentState.motion(event.x, event.y)
510
511    def leave_notify(self, widget, event):
512        return self.currentState.leave(event.x, event.y)
513
514    def key_pressed(self, keyname):
515        if keyname in "PNBRQKMFSOox12345678abcdefgh":
516            self.keybuffer += keyname
517
518        elif keyname == "minus":
519            self.keybuffer += "-"
520
521        elif keyname == "at":
522            self.keybuffer += "@"
523
524        elif keyname == "equal":
525            self.keybuffer += "="
526
527        elif keyname == "Return":
528            color = self.view.model.boards[-1].color
529            board = self.view.model.getBoardAtPly(
530                self.view.shown, self.view.shown_variation_idx)
531            try:
532                move = parseAny(board, self.keybuffer)
533            except ParsingError:
534                self.keybuffer = ""
535                return
536
537            if validate(board, move):
538                if (self.view.model.curplayer.__type__ == LOCAL or self.view.model.examined) and \
539                        self.view.shownIsMainLine() and \
540                        self.view.model.boards[-1] == board and \
541                        self.view.model.status == RUNNING:
542                    # emit move
543                    self.emit("piece_moved", move, color)
544                    if self.view.model.examined:
545                        self.view.model.connection.bm.sendMove(toAN(board, move))
546                else:
547                    self.play_or_add_move(board, move)
548            self.keybuffer = ""
549
550        elif keyname == "BackSpace":
551            self.keybuffer = self.keybuffer[:-1] if self.keybuffer else ""
552
553    def _genPossibleBoards(self, ply):
554        possible_boards = []
555        if self.setup_position:
556            return possible_boards
557        if len(self.view.model.players) == 2 and self.view.model.isEngine2EngineGame():
558            return possible_boards
559        curboard = self.view.model.getBoardAtPly(ply,
560                                                 self.view.shown_variation_idx)
561        for lmove_item in lmovegen.genAllMoves(curboard.board.clone()):
562            move = Move(lmove_item)
563            board = curboard.move(move)
564            possible_boards.append(board)
565        return possible_boards
566
567
568class BoardState:
569    """
570    There are 6 total BoardStates:
571    NormalState, ActiveState, SelectedState
572    LockedNormalState, LockedActiveState, LockedSelectedState
573
574    The board state is Locked while it is the opponents turn.
575    The board state is not Locked during your turn.
576    (Locked states are not used when BoardControl setup_position is True.)
577
578    Normal/Locked State - No pieces or cords are selected
579    Active State - A piece is currently being dragged by the mouse
580    Selected State - A cord is currently selected
581    """
582
583    def __init__(self, board):
584        self.parent = board
585        self.view = board.view
586        self.lastMotionCord = None
587
588        self.RANKS = self.view.model.boards[0].RANKS
589        self.FILES = self.view.model.boards[0].FILES
590
591    def getBoard(self):
592        return self.view.model.getBoardAtPly(self.view.shown,
593                                             self.view.shown_variation_idx)
594
595    def validate(self, cord0, cord1):
596        if cord0 is None or cord1 is None:
597            return False
598        # prevent accidental NULL_MOVE creation
599        if cord0 == cord1 and self.parent.variant.variant != SITTUYINCHESS:
600            return False
601        if self.getBoard()[cord0] is None:
602            return False
603
604        if self.parent.setup_position:
605            # prevent moving pieces inside holding
606            if (cord0.x < 0 or cord0.x > self.FILES - 1) and \
607                    (cord1.x < 0 or cord1.x > self.FILES - 1):
608                return False
609            else:
610                return True
611
612        if cord1.x < 0 or cord1.x > self.FILES - 1:
613            return False
614        if cord0.x < 0 or cord0.x > self.FILES - 1:
615            # drop
616            return validate(self.getBoard(), Move(lmovegen.newMove(
617                self.getBoard()[cord0].piece, cord1.cord, DROP)))
618        else:
619            return validate(self.getBoard(), Move(cord0, cord1,
620                                                  self.getBoard()))
621
622    def transPoint(self, x_loc, y_loc):
623        xc_loc, yc_loc, side = self.view.square[0], self.view.square[1], \
624            self.view.square[3]
625        x_loc, y_loc = self.view.invmatrix.transform_point(x_loc, y_loc)
626        y_loc -= yc_loc
627        x_loc -= xc_loc
628
629        y_loc /= float(side)
630        x_loc /= float(side)
631        return x_loc, self.RANKS - y_loc
632
633    def point2Cord(self, x_loc, y_loc, color=None):
634        point = self.transPoint(x_loc, y_loc)
635        p0_loc, p1_loc = point[0], point[1]
636        if self.parent.variant.variant in DROP_VARIANTS:
637            if not-3 <= int(p0_loc) <= self.FILES + 2 or not 0 <= int(
638                    p1_loc) <= self.RANKS - 1:
639                return None
640        else:
641            if not 0 <= int(p0_loc) <= self.FILES - 1 or not 0 <= int(
642                    p1_loc) <= self.RANKS - 1:
643                return None
644        return Cord(int(p0_loc) if p0_loc >= 0 else int(p0_loc) - 1, int(p1_loc), color)
645
646    def isSelectable(self, cord):
647        # Simple isSelectable method, disabling selecting cords out of bound etc
648        if not cord:
649            return False
650        if self.parent.setup_position:
651            return True
652        if self.parent.variant.variant in DROP_VARIANTS:
653            if (not-3 <= cord.x <= self.FILES + 2) or (
654                    not 0 <= cord.y <= self.RANKS - 1):
655                return False
656        else:
657            if (not 0 <= cord.x <= self.FILES - 1) or (
658                    not 0 <= cord.y <= self.RANKS - 1):
659                return False
660        return True
661
662    def press(self, x_loc, y_loc, button):
663        pass
664
665    def release(self, x_loc, y_loc):
666        pass
667
668    def motion(self, x_loc, y_loc):
669        cord = self.point2Cord(x_loc, y_loc)
670        if self.lastMotionCord == cord:
671            return
672        self.lastMotionCord = cord
673        if cord and self.isSelectable(cord):
674            if not self.view.model.isPlayingICSGame():
675                self.view.hover = cord
676        else:
677            self.view.hover = None
678
679    def leave(self, x_loc, y_loc):
680        allocation = self.parent.get_allocation()
681        if not (0 <= x_loc < allocation.width and 0 <= y_loc < allocation.height):
682            self.view.hover = None
683
684
685class LockedBoardState(BoardState):
686    '''
687    Parent of LockedNormalState, LockedActiveState, LockedSelectedState
688
689    The board is in one of the three Locked states during the opponent's turn.
690    '''
691
692    def __init__(self, board):
693        BoardState.__init__(self, board)
694
695    def isAPotentiallyLegalNextMove(self, cord0, cord1):
696        """ Determines whether the given move is at all legally possible
697            as the next move after the player who's turn it is makes their move
698            Note: This doesn't always return the correct value, such as when
699            BoardControl.setLocked() has been called and we've begun a drag,
700            but view.shown and BoardControl.lockedPly haven't been updated yet """
701        if cord0 is None or cord1 is None:
702            return False
703        if self.parent.lockedPly not in self.parent.possibleBoards:
704            return False
705        for board in self.parent.possibleBoards[self.parent.lockedPly]:
706            if not board[cord0]:
707                return False
708            if validate(board, Move(cord0, cord1, board)):
709                return True
710        return False
711
712
713class NormalState(BoardState):
714    '''
715    It is the human player's turn and no pieces or cords are selected.
716    '''
717
718    def isSelectable(self, cord):
719        if not BoardState.isSelectable(self, cord):
720            return False
721        if self.parent.setup_position:
722            return True
723        try:
724            board = self.getBoard()
725            if board[cord] is None:
726                return False  # We don't want empty cords
727            elif board[cord].color != board.color:
728                return False  # We shouldn't be able to select an opponent piece
729        except IndexError:
730            return False
731        return True
732
733    def press(self, x_loc, y_loc, button):
734        self.parent.grab_focus()
735        cord = self.point2Cord(x_loc, y_loc)
736        if self.isSelectable(cord):
737            self.view.dragged_piece = self.getBoard()[cord]
738            self.view.active = cord
739            self.parent.setStateActive()
740
741
742class ActiveState(BoardState):
743    '''
744    It is the human player's turn and a piece is being dragged by the mouse.
745    '''
746
747    def isSelectable(self, cord):
748        if not BoardState.isSelectable(self, cord):
749            return False
750        if self.parent.setup_position:
751            return True
752        return self.validate(self.view.active, cord)
753
754    def release(self, x_loc, y_loc):
755        cord = self.point2Cord(x_loc, y_loc)
756        if self.view.selected and cord != self.view.active and not \
757                self.validate(self.view.selected, cord):
758            if not self.parent.setup_position:
759                preferencesDialog.SoundTab.playAction("invalidMove")
760        if not cord:
761            self.view.active = None
762            self.view.selected = None
763            self.view.dragged_piece = None
764            self.view.startAnimation()
765            self.parent.setStateNormal()
766
767        # When in the mixed active/selected state
768        elif self.view.selected:
769            # Move when releasing on a good cord
770            if self.validate(self.view.selected, cord):
771                self.parent.setStateNormal()
772                # It is important to emit_move_signal after setting state
773                # as listeners of the function probably will lock the board
774                self.view.dragged_piece = None
775                self.parent.emit_move_signal(self.view.selected, cord)
776                if self.parent.setup_position:
777                    if not (self.view.selected.x < 0 or
778                            self.view.selected.x > self.FILES - 1):
779                        self.view.selected = None
780                    else:
781                        # enable stamping with selected holding pieces
782                        self.parent.setStateSelected()
783                else:
784                    self.view.selected = None
785                self.view.active = None
786            elif cord == self.view.active == self.view.selected == self.parent.selected_last:
787                # user clicked (press+release) same piece twice, so unselect it
788                self.view.active = None
789                self.view.selected = None
790                self.view.dragged_piece = None
791                self.view.startAnimation()
792                self.parent.setStateNormal()
793                if self.parent.variant.variant == SITTUYINCHESS:
794                    self.parent.emit_move_signal(self.view.selected, cord)
795            else:  # leave last selected piece selected
796                self.view.active = None
797                self.view.dragged_piece = None
798                self.view.startAnimation()
799                self.parent.setStateSelected()
800
801        # If dragged and released on a possible cord
802        elif self.validate(self.view.active, cord):
803            self.parent.setStateNormal()
804            self.view.dragged_piece = None
805            # removig piece from board
806            if self.parent.setup_position and (cord.x < 0 or cord.x > self.FILES - 1):
807                self.view.startAnimation()
808            self.parent.emit_move_signal(self.view.active, cord)
809            self.view.active = None
810
811        # Select last piece user tried to move or that was selected
812        elif self.view.active or self.view.selected:
813            self.view.selected = self.view.active if self.view.active else self.view.selected
814            self.view.active = None
815            self.view.dragged_piece = None
816            self.view.startAnimation()
817            self.parent.setStateSelected()
818
819        # Send back, if dragging to a not possible cord
820        else:
821            self.view.active = None
822            # Send the piece back to its original cord
823            self.view.dragged_piece = None
824            self.view.startAnimation()
825            self.parent.setStateNormal()
826
827        self.parent.selected_last = self.view.selected
828
829    def motion(self, x_loc, y_loc):
830        BoardState.motion(self, x_loc, y_loc)
831        fcord = self.view.active
832        if not fcord:
833            return
834        piece = self.getBoard()[fcord]
835        if not piece:
836            return
837        elif piece.color != self.getBoard().color:
838            if not self.parent.setup_position:
839                return
840
841        side = self.view.square[3]
842        co_loc, si_loc = self.view.matrix[0], self.view.matrix[1]
843        point = self.transPoint(x_loc - side * (co_loc + si_loc) / 2.,
844                                y_loc + side * (co_loc - si_loc) / 2.)
845        if not point:
846            return
847        x_loc, y_loc = point
848
849        if piece.x != x_loc or piece.y != y_loc:
850            if piece.x:
851                paintbox = self.view.cord2RectRelative(piece.x, piece.y)
852            else:
853                paintbox = self.view.cord2RectRelative(self.view.active)
854            paintbox = join(paintbox, self.view.cord2RectRelative(x_loc, y_loc))
855            piece.x = x_loc
856            piece.y = y_loc
857            self.view.redrawCanvas(rect(paintbox))
858
859
860class SelectedState(BoardState):
861    '''
862    It is the human player's turn and a cord is selected.
863    '''
864
865    def isSelectable(self, cord):
866        if not BoardState.isSelectable(self, cord):
867            return False
868        if self.parent.setup_position:
869            return True
870        try:
871            board = self.getBoard()
872            if board[cord] is not None and board[cord].color == board.color:
873                return True  # Select another piece
874        except IndexError:
875            return False
876        return self.validate(self.view.selected, cord)
877
878    def press(self, x_loc, y_loc, button):
879        cord = self.point2Cord(x_loc, y_loc)
880        # Unselecting by pressing the selected cord, or marking the cord to be
881        # moved to. We don't unset self.view.selected, so ActiveState can handle
882        # things correctly
883        if self.isSelectable(cord):
884            if self.parent.setup_position:
885                color_ok = True
886            else:
887                color_ok = self.getBoard()[cord] is not None and \
888                    self.getBoard()[cord].color == self.getBoard().color
889            if self.view.selected and self.view.selected != cord and \
890               color_ok and not self.validate(self.view.selected, cord):
891                # corner case encountered:
892                # user clicked (press+release) a piece, then clicked (no release yet)
893                # a different piece and dragged it somewhere else. Since
894                # ActiveState.release() will use self.view.selected as the source piece
895                # rather than self.view.active, we need to update it here
896                self.view.selected = cord  # re-select new cord
897
898            self.view.dragged_piece = self.getBoard()[cord]
899            self.view.active = cord
900            self.parent.setStateActive()
901
902        else:  # Unselecting by pressing an inactive cord
903            self.view.selected = None
904            self.parent.setStateNormal()
905            if not self.parent.setup_position:
906                preferencesDialog.SoundTab.playAction("invalidMove")
907
908
909class LockedNormalState(LockedBoardState):
910    '''
911    It is the opponent's turn and no piece or cord is selected.
912    '''
913
914    def isSelectable(self, cord):
915        if not BoardState.isSelectable(self, cord):
916            return False
917        if not self.parent.allowPremove:
918            return False  # Don't allow premove if neither player is human
919        try:
920            board = self.getBoard()
921            if board[cord] is None:
922                return False  # We don't want empty cords
923            elif board[cord].color == board.color:
924                return False  # We shouldn't be able to select an opponent piece
925        except IndexError:
926            return False
927        return True
928
929    def press(self, x, y, button):
930        self.parent.grab_focus()
931        cord = self.point2Cord(x, y)
932        if self.isSelectable(cord):
933            self.view.dragged_piece = self.getBoard()[cord]
934            self.view.active = cord
935            self.parent.setStateActive()
936
937        # reset premove if mouse right-clicks or clicks one of the premove cords
938        if button == 3:  # right-click
939            self.view.setPremove(None, None, None, None)
940            self.view.startAnimation()
941        elif cord == self.view.premove0 or cord == self.view.premove1:
942            self.view.setPremove(None, None, None, None)
943            self.view.startAnimation()
944
945
946class LockedActiveState(LockedBoardState):
947    '''
948    It is the opponent's turn and a piece is being dragged by the mouse.
949    '''
950
951    def isSelectable(self, cord):
952        if not BoardState.isSelectable(self, cord):
953            return False
954        return self.isAPotentiallyLegalNextMove(self.view.active, cord)
955
956    def release(self, x_loc, y_loc):
957        cord = self.point2Cord(x_loc, y_loc)
958        if cord == self.view.active == self.view.selected == self.parent.selected_last:
959            # User clicked (press+release) same piece twice, so unselect it
960            self.view.active = None
961            self.view.selected = None
962            self.view.dragged_piece = None
963            self.view.startAnimation()
964            self.parent.setStateNormal()
965        elif self.parent.allowPremove and self.view.selected and self.isAPotentiallyLegalNextMove(
966                self.view.selected, cord):
967            # In mixed locked selected/active state and user selects a valid premove cord
968            board = self.getBoard()
969            if board[self.view.selected].sign == PAWN and \
970                    cord.cord in board.PROMOTION_ZONE[1 - board.color]:
971                if conf.get("autoPromote"):
972                    promotion = lmove.PROMOTE_PIECE(QUEEN_PROMOTION)
973                else:
974                    promotion = self.parent.getPromotion()
975            else:
976                promotion = None
977            self.view.setPremove(board[self.view.selected], self.view.selected,
978                                 cord, self.view.shown + 2, promotion)
979            self.view.selected = None
980            self.view.active = None
981            self.view.dragged_piece = None
982            self.view.startAnimation()
983            self.parent.setStateNormal()
984        elif self.parent.allowPremove and self.isAPotentiallyLegalNextMove(
985                self.view.active, cord):
986            # User drags a piece to a valid premove square
987            board = self.getBoard()
988            if board[self.view.active].sign == PAWN and \
989                    cord.cord in board.PROMOTION_ZONE[1 - board.color]:
990                if conf.get("autoPromote"):
991                    promotion = lmove.PROMOTE_PIECE(QUEEN_PROMOTION)
992                else:
993                    promotion = self.parent.getPromotion()
994            else:
995                promotion = None
996            self.view.setPremove(self.getBoard()[self.view.active],
997                                 self.view.active, cord, self.view.shown + 2,
998                                 promotion)
999            self.view.selected = None
1000            self.view.active = None
1001            self.view.dragged_piece = None
1002            self.view.startAnimation()
1003            self.parent.setStateNormal()
1004        elif self.view.active or self.view.selected:
1005            # Select last piece user tried to move or that was selected
1006            self.view.selected = self.view.active if self.view.active else self.view.selected
1007            self.view.active = None
1008            self.view.dragged_piece = None
1009            self.view.startAnimation()
1010            self.parent.setStateSelected()
1011        else:
1012            self.view.active = None
1013            self.view.selected = None
1014            self.view.dragged_piece = None
1015            self.view.startAnimation()
1016            self.parent.setStateNormal()
1017
1018        self.parent.selected_last = self.view.selected
1019
1020    def motion(self, x_loc, y_loc):
1021        BoardState.motion(self, x_loc, y_loc)
1022        fcord = self.view.active
1023        if not fcord:
1024            return
1025        piece = self.getBoard()[fcord]
1026        if not piece or piece.color == self.getBoard().color:
1027            return
1028
1029        side = self.view.square[3]
1030        co_loc, si_loc = self.view.matrix[0], self.view.matrix[1]
1031        point = self.transPoint(x_loc - side * (co_loc + si_loc) / 2.,
1032                                y_loc + side * (co_loc - si_loc) / 2.)
1033        if not point:
1034            return
1035        x_loc, y_loc = point
1036
1037        if piece.x != x_loc or piece.y != y_loc:
1038            if piece.x:
1039                paintbox = self.view.cord2RectRelative(piece.x, piece.y)
1040            else:
1041                paintbox = self.view.cord2RectRelative(self.view.active)
1042            paintbox = join(paintbox, self.view.cord2RectRelative(x_loc, y_loc))
1043            piece.x = x_loc
1044            piece.y = y_loc
1045            self.view.redrawCanvas(rect(paintbox))
1046
1047
1048class LockedSelectedState(LockedBoardState):
1049    '''
1050    It is the opponent's turn and a cord is selected.
1051    '''
1052
1053    def isSelectable(self, cord):
1054        if not BoardState.isSelectable(self, cord):
1055            return False
1056        try:
1057            board = self.getBoard()
1058            if board[cord] is not None and board[cord].color != board.color:
1059                return True  # Select another piece
1060        except IndexError:
1061            return False
1062        return self.isAPotentiallyLegalNextMove(self.view.selected, cord)
1063
1064    def motion(self, x_loc, y_loc):
1065        cord = self.point2Cord(x_loc, y_loc)
1066        if self.lastMotionCord == cord:
1067            self.view.hover = cord
1068            return
1069        self.lastMotionCord = cord
1070        if cord and self.isAPotentiallyLegalNextMove(self.view.selected, cord):
1071            if not self.view.model.isPlayingICSGame():
1072                self.view.hover = cord
1073        else:
1074            self.view.hover = None
1075
1076    def press(self, x_loc, y_loc, button):
1077        cord = self.point2Cord(x_loc, y_loc)
1078        # Unselecting by pressing the selected cord, or marking the cord to be
1079        # moved to. We don't unset self.view.selected, so ActiveState can handle
1080        # things correctly
1081        if self.isSelectable(cord):
1082            if self.view.selected and self.view.selected != cord and \
1083               self.getBoard()[cord] is not None and \
1084               self.getBoard()[cord].color != self.getBoard().color and \
1085               not self.isAPotentiallyLegalNextMove(self.view.selected, cord):
1086                # corner-case encountered (see comment in SelectedState.press)
1087                self.view.selected = cord  # re-select new cord
1088
1089            self.view.dragged_piece = self.getBoard()[cord]
1090            self.view.active = cord
1091            self.parent.setStateActive()
1092
1093        else:  # Unselecting by pressing an inactive cord
1094            self.view.selected = None
1095            self.parent.setStateNormal()
1096
1097        # reset premove if mouse right-clicks or clicks one of the premove cords
1098        if button == 3:  # right-click
1099            self.view.setPremove(None, None, None, None)
1100            self.view.startAnimation()
1101        elif cord == self.view.premove0 or cord == self.view.premove1:
1102            self.view.setPremove(None, None, None, None)
1103            self.view.startAnimation()
1104