Рубрики
Без рубрики

Написание шахматной игры в Python [День 4]

Мы все еще в блокировке из-за Covid-19, и я использую время, чтобы реализовать текстовые шахматы … Теги с Python, CodeNewie.

Мы все еще находимся в блокировке из-за Covid-19, и я использую время для реализации текстовой шахматной игры с моим 12-летним. В этой серии я буду делиться тем, что мы сделали, и как проект развивался.

Мы остановились в 3-й день с действительным ходом, проверяющие тестовые набор для общих сценариев (целевой квадрат вне границ или занятых куском с одинаковым цветом), а в ладьях.

Сегодня мы решили внедрить перемещение ладья на доске.

Мы определили этот интерфейс:

def move(board, piece, target_rank, target_file):
  pass

Когда мы рассмотрели то, что нам понадобится, чтобы реализовать его, мы поняли, что проверка, если будет действительный ход, будет недостаточно, мы должны были бы знать, где находилась действительная часть. А поскольку IS_Valid_Move Вычисляет это местоположение как часть проверки проверки – оно имело гораздо больше смысла объединить их вместо реализации второго метода в дополнение к методу проверки.

Если честно, я знал, что это то, что мы бы в конечном итоге делаем, но я позволил ему понять эту часть самостоятельно, это часть процесса обучения. Дополнительное преимущество заключается в том, что он показал ему ценность проведения тестов – после того, как мы изменили метод, чтобы вернуть что -то еще, мы внесли несколько небольших изменений в существующих тестах, и когда они пройдут, мы могли бы быть уверенными, что не вызвали никакой регрессии в функциональность.

Вот что мы изменили:

def find_valid_move(board, piece, target_rank, target_file):
  # out of bounds
  if  target_rank < 0 or target_rank > 7:
    return -1, -1
  if  target_file < 0 or target_file > 7:
    return -1, -1
  # piece with same color is in the target square
  if board[target_rank][target_file][0] == piece[0]:
    return -1, -1

  if piece in ("WR", "BR"):
    return find_valid_rook_move(board, piece, target_rank, target_file)

  return -1, -1

def find_valid_rook_move(board, piece, target_rank, target_file):
  found_rank = -1
  found_file = -1
  for i in range(8):
    if board[target_rank][i] == piece:
      found_rank = target_rank
      found_file = i
      break

  if found_rank == -1:
    for i in range(8):
      if board[i][target_file] == piece:
        found_rank = i
        found_file = target_file
        break

  if found_rank < 0 or found_file < 0:
    return -1, -1 

  if found_rank == target_rank:
    start_file = min(target_file+1, found_file+1)
    end_file = max(target_file, found_file)
    for i in range(start_file, end_file):
        if board[target_rank][i] != EMPTY:
          return -1, -1
  else: # found_file == target_file
    start_rank = min(target_rank+1, found_rank+1)
    end_rank = max(target_rank, found_rank)
    for i in range(start_rank, end_rank):
        if board[i][target_file] != EMPTY:
          return -1, -1
  return found_rank, found_file

Это та же самая логика, только мы возвращаем (-1, -1), когда не найдено допустимого, и (supd_rank, found_file), если мы нашли допустимую часть.

Далее мы реализовали переехать Логика следующим образом:

def move(board, piece, target_rank, target_file):
  found_rank, found_file = find_valid_move(board, piece, 
  target_rank, target_file)
  if (found_rank, found_file) != (-1, -1):
    board[target_rank][target_file] = piece
    board[found_rank][found_file] = EMPTY
    return True
  return False

Далее мы сделали небольшой ручной тест, чтобы увидеть это двигаться Работал, как и ожидалось:

board = create_board()
board[1][0] = EMPTY
result = move(board, "WR", 5, 0)
print_board(board)

И удивительный выход был:

   ---- ---- ---- ---- ---- ---- ---- ----
8 | BR | BN | BB | BQ | BK | BB | BN | BR |
   ---- ---- ---- ---- ---- ---- ---- ----
7 | BP | BP | BP | BP | BP | BP | BP | BP |
   ---- ---- ---- ---- ---- ---- ---- ----
6 | WR |    |    |    |    |    |    |    |
   ---- ---- ---- ---- ---- ---- ---- ----
5 | WR |    |    |    |    |    |    |    |
   ---- ---- ---- ---- ---- ---- ---- ----
4 | WR |    |    |    |    |    |    |    |
   ---- ---- ---- ---- ---- ---- ---- ----
3 | WR |    |    |    |    |    |    |    |
   ---- ---- ---- ---- ---- ---- ---- ----
2 |    | WP | WP | WP | WP | WP | WP | WP |
   ---- ---- ---- ---- ---- ---- ---- ----
1 |    | WN | WB | WQ | WK | WB | WN | WR |
   ---- ---- ---- ---- ---- ---- ---- ----
    a    b    c    d    e    f    g    h  

Не было никакого способа, которым 12 лет выяснили бы это, но я понял, что произошло сразу, и я должен был знать лучше. Упс Я инициализировал доску, используя:

board = [[EMPTY]*8]*8

Который создает один список и дублирует его 8 раз. Когда мы что-то изменили в ранг 5, он изменил его во всех средних строках (другие строки были заменены строками, которые содержали кусочки). Очень неправильно. Не делай этого.

12 лет были некоторые проблемы с пониманием того, что пошло не так и почему, поэтому я создал этот простой пример, чтобы объяснить:

sub_list = [1,2,3]
test_list = [sub_list]*3
print(test_list)
sub_list[0] = 5
print(test_list)

И он рода пошел “ООО« Ооооххх, да … это изменит товар во всех списках одновременно » Поэтому я понял, что он понял. Выход:

[[1, 2, 3], [1, 2, 3], [1, 2, 3]]
[[5, 2, 3], [5, 2, 3], [5, 2, 3]]

Как только мы исправим это, это работало правильно. 12во написал следующий тест:

def test_move_rook():
  board = create_board()
  board[1][0] = EMPTY
  result = move(board, "WR", 5, 0)
  assert result
  print("test_move_rook passed")

Неплохо! Однако я спросил его, если бы это нашло бы проблему, которую мы видели, когда мы провели тест вручную. Он немного подумал об этом и сказал «нет». Я добавил ОБЕСПЕЧЕНИЕ_BOOD бит к тесту:

def test_move_rook():
  board = create_board()
  board[1][0] = EMPTY

  expected_board = create_board()
  expected_board[1][0] = EMPTY
  expected_board[0][0] = EMPTY
  expected_board[5][0] = "WR"

  result = move(board, "WR", 5, 0)
  assert result
  assert board == expected_board
  print("test_move_rook passed")

Я знал, что это придет Но я не хотел добавлять его в микс слишком рано … Теперь было время: что произойдет, если есть две достоверные произведения? 12во начинал сожалеть, что не примет мое первоначальное представление о том, чтобы оперить время, когда мы двигали кусок от а также цель Вместо того, чтобы рассчитать возможные места, но я не собирался сдаваться сейчас.

Он сказал, что, когда есть две подходящие части, игроки указывают ранг или файл произведения, но чтобы сделать вещи чуть проще, я предложил нам указать ранг и файл. Когда он видел, что мой совет обычно звучит, он решил принять эту идею.

Мы изменили find_valid_rook_move метод следующим образом:

  1. Мы добавили Source_rank и source_file Чтобы включить указание который Товар, который мы хотели переместить.
  2. Вместо того, чтобы остановить, как только мы нашли подходящую часть, мы добавили его в список found_pieces
  3. Если не было найдено, возвращайте -1, -1
  4. Если мы нашли более одного куска, попробуйте найти тот, который соответствует данному Source_rank и source_file Отказ
  5. Если есть только одно соответствие, используйте его.
  6. Затем продолжите и проверьте, не будет ли другого блокировки.
def find_valid_rook_move(board, piece, target_rank, target_file, source_rank=-1, source_file=-1):
  found_pieces = []
  for i in range(8):
    if board[target_rank][i] == piece:
      found_rank = target_rank
      found_file = i
      found_pieces.append((found_rank, found_file))

  for i in range(8):
    if board[i][target_file] == piece:
      found_rank = i
      found_file = target_file
      found_pieces.append((found_rank, found_file))

  if len(found_pieces) == 0:
    return -1, -1 

  # don't know which item to choose
  found_rank = -1
  found_file = -1
  if (len(found_pieces) > 1):
    if source_rank < 0 or source_file < 0:
      return -1, -1
    for rank, file in found_pieces:
      if rank == source_rank and file == source_file:
        found_rank = rank
        found_file = file
  else:
    found_rank, found_file = found_pieces[0]

  if (found_rank, found_file) == (-1, -1):
    return -1, -1 

  if found_rank == target_rank:
    start_file = min(target_file+1, found_file+1)
    end_file = max(target_file, found_file)
    for i in range(start_file, end_file):
        if board[target_rank][i] != EMPTY:
          return -1, -1
  else: # found_file == target_file
    start_rank = min(target_rank+1, found_rank+1)
    end_rank = max(target_rank, found_rank)
    for i in range(start_rank, end_rank):
        if board[i][target_file] != EMPTY:
          return -1, -1
  return found_rank, found_file

И добавил тест:

def test_double_move_rook():
  board = create_board()
  board[1][0] = EMPTY
  board[0][7] = EMPTY
  board[5][0] = "WR"
  result = move(board, "WR", 3, 0)
  assert not result

  expected_board = create_board()
  expected_board[1][0] = EMPTY
  expected_board[0][7] = EMPTY
  expected_board[5][0] = "WR"
  expected_board[3][0] = "WR"
  expected_board[0][0] = EMPTY
  result = move(board, "WR", 3, 0, 0, 0)
  assert result
  assert board == expected_board

  print("test_double_move_rook passed")

Есть еще один сценарий, однако мы должны проверить, и я уже знаю, что это не пройдет … Что произойдет, если на доске есть две подходящие части, но один из них заблокирован? Я позволил 12во написать тест, а затем попытаться понять, почему он не удался.

  ...
  board = create_board()
  board[0][7] = EMPTY
  board[5][0] = "WR"
  expected_board = create_board()
  expected_board[0][7] = EMPTY
  expected_board[3][0] = "WR"
  expected_board[0][0] = "WR"
  result = move(board, "WR", 3, 0)
  assert result
  assert board == expected_board

Мы добавили Печать Команды в разных местах в find_valid_rook_move И мы обнаружили, что с тех пор, как мы проверяем достоверность после Выбирая кусок, мы приходили к выводу у нас не было достаточно информации (то есть Source_rank и source_file слишком рано).

Мы переключились вокруг логики, чтобы найти Valid_pieces Перед проверкой нам нужна Source_rank и source_file , и это сработало (12 лет внес эти изменения после того, как я прошел его через логику 👏):

def find_valid_rook_move(board, piece, target_rank, target_file, source_rank=-1, source_file=-1):
  found_pieces = []
  for i in range(8):
    if board[target_rank][i] == piece:
      found_rank = target_rank
      found_file = i
      found_pieces.append((found_rank, found_file))

  for i in range(8):
    if board[i][target_file] == piece:
      found_rank = i
      found_file = target_file
      found_pieces.append((found_rank, found_file))

  if len(found_pieces) == 0:
    return -1, -1 

  valid_pieces = []

  for found_rank, found_file in found_pieces:
    is_valid = True
    if found_rank == target_rank:
      start_file = min(target_file+1, found_file+1)
      end_file = max(target_file, found_file)
      for i in range(start_file, end_file):
          if board[target_rank][i] != EMPTY:
            is_valid = False
            break
    else: # found_file == target_file
      start_rank = min(target_rank+1, found_rank+1)
      end_rank = max(target_rank, found_rank)
      for i in range(start_rank, end_rank):
          if board[i][target_file] != EMPTY:
            is_valid = False
            break
    if is_valid:
      valid_pieces.append((found_rank, found_file))

  # don't know which item to choose
  found_rank = -1
  found_file = -1
  if len(valid_pieces) > 1:
    if source_rank < 0 or source_file < 0:
      return -1, -1
    for rank, file in valid_pieces:
      if rank == source_rank and file == source_file:
        found_rank = rank
        found_file = file
  elif len(valid_pieces) == 1:
    found_rank, found_file = valid_pieces[0]

  return found_rank, found_file

Теперь мы все настроем с шаблоном для реализации перемещения всех остальных произведений, поэтому я надеюсь, что мы сможем пройти через следующие быстрее.

Присоединяйтесь к нам в следующий раз!

Окончательный код на сегодняшний день (включая модифицированные тесты):

EMPTY = "  "

def print_board(board):
  row_number = 8
  print("  ", end="")
  print(" ----"*8)
  for row in reversed(board):
      print(row_number, end=" ")
      row_number -= 1
      for cell in row:
          print("| {} ".format(cell), end="")
      print("|")
      print("  ", end="")
      print(" ----"*8)
  print("  ", end="")
  for letter in ['a','b','c','d','e','f','g','h']:
      print("  {}  ".format(letter), end="")
  print("")

def create_board():
  board = []

  board.append(["WR","WN","WB","WQ","WK","WB","WN","WR"])
  board.append(["WP","WP","WP","WP","WP","WP","WP","WP"])
  for i in range(2, 6):
    board.append([EMPTY]*8)
  board.append(["BP","BP","BP","BP","BP","BP","BP","BP"])
  board.append(["BR","BN","BB","BQ","BK","BB","BN","BR"])

  return board

def find_valid_move(board, piece, target_rank, target_file, source_rank=-1, source_file=-1):
  # out of bounds
  if  target_rank < 0 or target_rank > 7:
    return -1, -1
  if  target_file < 0 or target_file > 7:
    return -1, -1
  # piece with same color is in the target cell
  if board[target_rank][target_file][0] == piece[0]:
    return -1, -1

  if piece in ("WR", "BR"):
    return find_valid_rook_move(board, piece, target_rank, target_file, source_rank, source_file)

  return -1, -1

def find_valid_rook_move(board, piece, target_rank, target_file, source_rank=-1, source_file=-1):
  found_pieces = []
  for i in range(8):
    if board[target_rank][i] == piece:
      found_rank = target_rank
      found_file = i
      found_pieces.append((found_rank, found_file))

  for i in range(8):
    if board[i][target_file] == piece:
      found_rank = i
      found_file = target_file
      found_pieces.append((found_rank, found_file))

  if len(found_pieces) == 0:
    return -1, -1 

  valid_pieces = []

  for found_rank, found_file in found_pieces:
    is_valid = True
    if found_rank == target_rank:
      start_file = min(target_file+1, found_file+1)
      end_file = max(target_file, found_file)
      for i in range(start_file, end_file):
          if board[target_rank][i] != EMPTY:
            is_valid = False
            break
    else: # found_file == target_file
      start_rank = min(target_rank+1, found_rank+1)
      end_rank = max(target_rank, found_rank)
      for i in range(start_rank, end_rank):
          if board[i][target_file] != EMPTY:
            is_valid = False
            break
    if is_valid:
      valid_pieces.append((found_rank, found_file))

  # don't know which item to choose
  found_rank = -1
  found_file = -1
  if len(valid_pieces) > 1:
    if source_rank < 0 or source_file < 0:
      return -1, -1
    for rank, file in valid_pieces:
      if rank == source_rank and file == source_file:
        found_rank = rank
        found_file = file
  elif len(valid_pieces) == 1:
    found_rank, found_file = valid_pieces[0]

  return found_rank, found_file


def move(board, piece, target_rank, target_file, source_rank=-1, source_file=-1):
  found_rank, found_file = find_valid_move(board, piece, 
  target_rank, target_file, source_rank, source_file)
  if (found_rank, found_file) != (-1, -1):
    board[target_rank][target_file] = piece
    board[found_rank][found_file] = EMPTY
    return True
  return False

# ----------------------- tests -------------------

def test_move_rook():
  board = create_board()
  board[1][0] = EMPTY

  expected_board = create_board()
  expected_board[1][0] = EMPTY
  expected_board[0][0] = EMPTY
  expected_board[5][0] = "WR"

  result = move(board, "WR", 5, 0)
  assert result
  assert board == expected_board
  print("test_move_rook passed")

def test_double_move_rook():
  board = create_board()
  board[1][0] = EMPTY
  board[0][7] = EMPTY
  board[5][0] = "WR"
  result = move(board, "WR", 3, 0)
  assert not result

  expected_board = create_board()
  expected_board[1][0] = EMPTY
  expected_board[0][7] = EMPTY
  expected_board[5][0] = "WR"
  expected_board[3][0] = "WR"
  expected_board[0][0] = EMPTY
  result = move(board, "WR", 3, 0, 0, 0)
  assert result
  assert board == expected_board

  board = create_board()
  board[0][7] = EMPTY
  board[5][0] = "WR"
  expected_board = create_board()
  expected_board[0][7] = EMPTY
  expected_board[3][0] = "WR"
  expected_board[0][0] = "WR"
  result = move(board, "WR", 3, 0)
  assert result
  assert board == expected_board
  print("test_double_move_rook passed")

def test_is_rank_valid():
  board = create_board()
  result = find_valid_move(board, "xx", 22, 4)
  assert result == (-1, -1)
  result = find_valid_move(board, "xx", -2020, 4)
  assert result == (-1, -1)
  print("test_is_rank_valid passed")

def test_is_file_valid():
  board = create_board()
  result = find_valid_move(board, "xx", 0, 22)
  assert result == (-1, -1)
  result = find_valid_move(board, "xx", 0, -2020)
  assert result == (-1, -1)
  print("test_is_file_valid passed")

def test_is_target_square_taken():
  board = create_board()
  result = find_valid_move(board, "Wx", 0, 1)
  assert result == (-1, -1)
  print("test_is_target_square_taken passed")

def test_rook_is_blocked():
  board = create_board()
  result = find_valid_rook_move(board, "BR", 5, 0)
  assert result == (-1, -1)
  board[7][5] = EMPTY
  result = find_valid_rook_move(board, "BR", 7, 5)
  assert result == (-1, -1)
  print("test_rook_is_blocked passed")

def test_rook_is_not_blocked():
  board = create_board()
  board[1][0] = EMPTY
  result = find_valid_rook_move(board, "WR", 2, 0)
  assert result == (0,0)
  board[0][1] = EMPTY
  result = find_valid_rook_move(board, "WR", 0, 1)
  assert result == (0,0)
  print("test_rook_is_not_blocked passed")

def run_tests():
  test_is_rank_valid()
  test_is_file_valid()
  test_is_target_square_taken()
  test_rook_is_blocked()
  test_rook_is_not_blocked()
  test_move_rook()
  test_double_move_rook()
run_tests()

Оригинал: “https://dev.to/rinaarts/writing-a-chess-game-in-python-day-4-1o84”