def can_be_promotion(move: Move) -> bool: ktype = KomaType.get(move.koma) _, promotion_constrainer = MOVEGEN_FUNCTIONS[ktype] komatype_can_promote = (promotion_constrainer == constrain_unpromotable) return not move.is_drop and not komatype_can_promote and ( move.end_sq.is_in_promotion_zone(move.side) or move.start_sq.is_in_promotion_zone(move.side))
def _is_move_between_squares_valid(pos: Position, start_sq: Square, end_sq: Square) -> bool: koma = pos.get_koma(start_sq) ktype = KomaType.get(koma) board = pos.board side = koma.side() start_idx = MailboxBoard.sq_to_idx(start_sq) end_idx = MailboxBoard.sq_to_idx(end_sq) dest_generator, _ = MOVEGEN_FUNCTIONS[ktype] return end_idx in dest_generator(board, start_idx, side)
def unmake_move(self, move: Move) -> None: """Unplays/retracts a move from the board. """ if move.is_pass(): self.movenum -= 1 return elif move.is_drop: self.set_koma(Koma.NONE, move.end_sq) self.inc_hand_koma(move.side, KomaType.get(move.koma)) self.turn = self.turn.switch() self.movenum -= 1 return else: if move.captured != Koma.NONE: self.dec_hand_koma(move.side, KomaType.get(move.captured).unpromote()) self.set_koma(move.captured, move.end_sq) self.set_koma(move.koma, move.start_sq) self.turn = self.turn.switch() self.movenum -= 1 return
def get_ambiguous_moves(pos: Position, move: Move) -> List[Move]: start_sq = move.start_sq if not _is_move_from_square_available(pos, start_sq): return [] koma = pos.get_koma(start_sq) side = koma.side() ktype = KomaType.get(koma) return [ mv for mv in generate_valid_moves(pos, side, ktype) if (mv.end_sq == move.end_sq) and ( mv.start_sq != move.start_sq) and is_legal(mv, pos) ]
def _disambiguate_japanese_move( pos: Position, move: Move, ambiguous_moves: Iterable[Move], aggressive_disambiguation: bool, ) -> str: origin_squares = set( (amb_move.start_sq for amb_move in ambiguous_moves if (aggressive_disambiguation) or rules.can_be_promotion(amb_move) == rules.can_be_promotion(move))) general_pieces = set((KomaType.GI, KomaType.KI, KomaType.TO, KomaType.NY, KomaType.NK, KomaType.NG)) side = move.koma.side() start_sq = move.start_sq end_sq = move.end_sq # sugu is special case if KomaType.get(move.koma) in general_pieces: if end_sq.is_immediately_forward_of(start_sq, side): return "直" if _is_leftmost(start_sq, origin_squares, side): return "左" elif _is_rightmost(start_sq, origin_squares, side): return "右" elif end_sq.is_same_row(start_sq): sqs = [sq for sq in origin_squares if end_sq.is_same_row(sq)] return _disambiguate_character(start_sq, sqs, side) + "寄" elif end_sq.is_forward_of(start_sq, side): sqs = [] for sq in origin_squares: if end_sq.is_forward_of(sq, side) and not ( KomaType.get(move.koma) in general_pieces and end_sq.is_immediately_forward_of(sq, side)): sqs.append(sq) return _disambiguate_character(start_sq, sqs, side) + "上" elif end_sq.is_backward_of(start_sq, side): sqs = [sq for sq in origin_squares if end_sq.is_backward_of(sq, side)] return _disambiguate_character(start_sq, sqs, side) + "引" raise ValueError( f"Disambiguation failed unexpectedly on move: {str(move)}")
def _parse_sfen_hands(self, sfen_hands: str) -> None: it_hands = re.findall(r"(\d*)([plnsgbrPLNSGBR])", sfen_hands) for ch_count, ch in it_hands: try: koma = KOMA_FROM_SFEN[ch] except KeyError as exc: raise ValueError( f"SFEN contains unknown character '{ch}'") from exc ktype = KomaType.get(koma) target_hand = self.hand_sente if ch.isupper() else self.hand_gote count = int(ch_count) if ch_count else 1 target_hand.set_komatype_count(ktype, count) return
def draw(self): """Draw complete board with komadai and pieces. """ # Clear board display - could also keep board and just redraw pieces self.delete("all") position = self.position komadai_w = self.measurements.komadai_w coords_text_size = self.measurements.coords_text_size w_pad = self.measurements.w_pad x_sq = self.measurements.x_sq y_sq = self.measurements.y_sq north_side = (Side.SENTE if self._is_inverted(Side.SENTE) else Side.GOTE) south_side = north_side.switch() north_hand = position.get_hand_of_side(north_side) south_hand = position.get_hand_of_side(south_side) self._draw_canvas_base_layer() # Draw board self.board_artist.draw_board(self) self._add_board_onclick_callbacks() # Draw board pieces for koma, sqset in position.get_koma_sets().items(): for sq in sqset: col_idx, row_idx = self._sq_to_idxs(sq) ktype = KomaType.get(koma) invert = self._is_inverted(koma.side()) if self.is_text(): text = str(KANJI_FROM_KTYPE[ktype]) self.board_artist.draw_text_koma(self, text, invert, row_idx, col_idx) else: img = self.get_koma_image(ktype, invert) self.board_artist.draw_koma(self, img, row_idx, col_idx) # Draw komadai self.draw_komadai(w_pad + komadai_w / 2, y_sq(0), north_hand, sente=north_side.is_sente(), align="top") self.draw_komadai(x_sq(9) + 2 * coords_text_size + komadai_w / 2, y_sq(9), south_hand, sente=south_side.is_sente(), align="bottom") # set focus self.set_focus(self.highlighted_sq) self.board_artist.lift_click_layer(self) return
def make_move(self, move: Move) -> None: """Makes a move on the board. """ if move.is_pass(): # to account for game terminations or other passing moves self.movenum += 1 return elif move.is_drop: self.dec_hand_koma(move.side, KomaType.get(move.koma)) self.set_koma(move.koma, move.end_sq) self.turn = self.turn.switch() self.movenum += 1 return else: self.set_koma(Koma.NONE, move.start_sq) if move.captured != Koma.NONE: self.inc_hand_koma(move.side, KomaType.get(move.captured).unpromote()) self.set_koma( move.koma.promote() if move.is_promotion else move.koma, move.end_sq) self.turn = self.turn.switch() self.movenum += 1 return
def _draw_komadai_focus_tile( self, canvas: BoardCanvas, y_offset: float, ktype: KomaType, ) -> int: id_: int = canvas.create_image( self.x_anchor - (self.width / 5), self.y_anchor + y_offset, image="", anchor="center", tags=("komadai_focus", ktype.to_csa(), "sente" if self.is_sente else "gote"), ) return id_
def create_valid_moves_given_squares(pos: Position, start_sq: Square, end_sq: Square) -> List[Move]: if not _is_move_from_square_available(pos, start_sq): return [] if not _is_move_between_squares_valid(pos, start_sq, end_sq): return [] koma = pos.get_koma(start_sq) side = pos.turn if koma.side() != pos.turn: return [] _, promotion_constrainer = MOVEGEN_FUNCTIONS[KomaType.get(koma)] return [ pos.create_move(start_sq, end_sq, can_promote) for can_promote in promotion_constrainer(side, start_sq, end_sq) ]
def _draw_komadai_koma( self, canvas: BoardCanvas, y_offset: float, ktype: KomaType, ) -> int: artist = canvas.make_koma_artist(invert=False, komadai=True) id_: int = artist.draw_koma( canvas, self.x_anchor - (self.width / 5), self.y_anchor + y_offset, ktype=ktype, anchor="center", tags=("komadai_koma", ktype.to_csa(), "sente" if self.is_sente else "gote"), ) return id_
def _attempt_move(self, sq: Square) -> Optional[bool]: """Check if a legal move can be made. Returns True and sends out the move if it is, False if not. Returns None if more information is needed (e.g. choice of promotion/nonpromotion. """ mvlist: List[Move] = rules.create_legal_moves_given_squares( pos=self.position, start_sq=self.focused_sq, end_sq=sq) if not mvlist: return False elif len(mvlist) == 1: self._send_move(mvlist[0]) return True elif len(mvlist) == 2: # There is a promotion and nonpromotion option. # More info needed, GUI prompts for input. koma = self.position.get_koma(self.focused_sq) self.board_canvas.prompt_promotion(sq, KomaType.get(koma)) return None else: raise RuntimeError("Unexpected mvlist length in _attempt_move()")
def _set_state(self, key: str, sq: Square = Square.NONE, hand_ktype: KomaType = KomaType.NONE) -> None: """Set the state machine's state and take any actions needed. """ if (key == "ready") or (key == "disabled"): self.focused_sq = Square.NONE self.focused_ktype = KomaType.NONE self.board_canvas.set_focus(Square.NONE) elif key == "hand": self.focused_sq = Square.HAND self.focused_ktype = hand_ktype self.board_canvas.set_focus(sq, hand_ktype) elif key == "board": self.focused_sq = sq self.focused_ktype = KomaType.get(self.position.get_koma(sq)) self.board_canvas.set_focus(sq) # "wait_for_promotion" currently needs no action self.active_state = self.states[key] return
def prompt_promotion(self, sq: Square, ktype: KomaType) -> None: """Display the visual cues prompting user to choose promotion or non-promotion. """ id_cover = self.board_artist.draw_promotion_cover(self) col_idx, row_idx = self._sq_to_idxs(sq) invert = self._is_inverted(self.position.turn) id_promoted = self.board_artist.draw_promotion_prompt_koma( self, ktype.promote(), invert, row_idx, col_idx) id_unpromoted = self.board_artist.draw_promotion_prompt_koma( self, ktype, invert, row_idx + 1, col_idx) callback = functools.partial(self._prompt_promotion_callback, sq=sq, ktype=ktype) self.tag_bind(id_promoted, "<Button-1>", functools.partial(callback, is_promotion=True)) self.tag_bind(id_unpromoted, "<Button-1>", functools.partial(callback, is_promotion=False)) self.tag_bind(id_cover, "<Button-1>", functools.partial(callback, is_promotion=None)) return
def write_koma(self, koma: Koma) -> str: return KANJI_NOTATION_FROM_KTYPE[KomaType.get(koma)]
def test_king_is_not_promoted(self): self.assertFalse(KomaType.get(Koma.OU).is_promoted())