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