update port CHECKERS to python

Moved a lot of methods into the board class, documented the logic for the board evaluation heuristics.

Documented some known issues with the original BASIC source code.

I've noticed that the computer has resigned to me when it has legal, if bad, moves. A bug in my code? A bug in the BASIC code? Still need to investigate further.
This commit is contained in:
Dave LeCompte
2021-03-03 22:37:43 -08:00
parent 7e31c244d7
commit 2f1a3e8555
2 changed files with 286 additions and 240 deletions

View File

@@ -6,6 +6,7 @@ How about a nice game of checkers?
Ported by Dave LeCompte
"""
import collections
PAGE_WIDTH = 64
@@ -17,7 +18,12 @@ COMPUTER_PIECE = -1
COMPUTER_KING = -2
EMPTY_SPACE = 0
INVALID_MOVE = -99
TOP_ROW = 7
BOTTOM_ROW = 0
MoveRecord = collections.namedtuple(
"MoveRecord", ["quality", "start_x", "start_y", "dest_x", "dest_y"]
)
def print_centered(msg):
@@ -33,6 +39,28 @@ def print_header(title):
print()
def get_coordinates(prompt):
err_msg = "ENTER COORDINATES in X,Y FORMAT"
while True:
print(prompt)
response = input()
if "," not in response:
print(err_msg)
continue
try:
x, y = [int(c) for c in response.split(",")]
except ValueError as ve:
print(err_msg)
continue
return x, y
def is_legal_board_coordinate(x, y):
return (0 <= x <= 7) and (0 <= y <= 7)
class Board:
def __init__(self):
self.spaces = [[0 for y in range(8)] for x in range(8)]
@@ -66,6 +94,245 @@ class Board:
return s
def get_spaces(self):
for x in range(0, 8):
for y in range(0, 8):
yield x, y
def get_spaces_with_computer_pieces(self):
for x, y in self.get_spaces():
contents = self.spaces[x][y]
if contents < 0:
yield x, y
def get_spaces_with_human_pieces(self):
for x, y in self.get_spaces():
contents = self.spaces[x][y]
if contents > 0:
yield x, y
def get_legal_deltas_for_space(self, x, y):
contents = self.spaces[x][y]
if contents == COMPUTER_PIECE:
for delta_x in (-1, 1):
yield (delta_x, -1)
else:
for delta_x in (-1, 1):
for delta_y in (-1, 1):
yield (delta_x, delta_y)
def pick_computer_move(self):
move_record = None
for start_x, start_y in self.get_spaces_with_computer_pieces():
for delta_x, delta_y in self.get_legal_deltas_for_space(start_x, start_y):
new_move_record = self.check_move(start_x, start_y, delta_x, delta_y)
if new_move_record is None:
continue
if (move_record is None) or (new_move_record.quality > move_record.quality):
move_record = new_move_record
return move_record
def check_move(self, start_x, start_y, delta_x, delta_y):
new_x = start_x + delta_x
new_y = start_y + delta_y
if not is_legal_board_coordinate(new_x, new_y):
return None
contents = self.spaces[new_x][new_y]
if contents == EMPTY_SPACE:
return self.evaluate_move(start_x, start_y, new_x, new_y)
if contents < 0:
return None
# check jump landing space, which is an additional dx, dy from new_x, newy
landing_x = new_x + delta_x
landing_y = new_y + delta_y
if not is_legal_board_coordinate(landing_x, landing_y):
return None
if self.spaces[landing_x][landing_y] == EMPTY_SPACE:
return self.evaluate_move(start_x, start_y, landing_x, landing_y)
def evaluate_move(self, start_x, start_y, dest_x, dest_y):
quality = 0
if dest_y == 0 and self.spaces[start_x][start_y] == COMPUTER_PIECE:
# promoting is good
quality += 2
if abs(dest_y - start_y) == 2:
# jumps are good
quality += 5
if start_y == 7:
# prefer to defend back row
quality -= 2
if dest_x in (0, 7):
# moving to edge column
quality += 1
for delta_x in (-1, 1):
if not is_legal_board_coordinate(dest_x + delta_x, dest_y - 1):
continue
if self.spaces[dest_x + delta_x][dest_y - 1] < 0:
# moving into "shadow" of another computer piece
quality += 1
if not is_legal_board_coordinate(dest_x - delta_x, dest_y + 1):
continue
if (
(self.spaces[dest_x + delta_x][dest_y - 1] > 0)
and (self.spaces[dest_x - delta_x][dest_y + 1] == EMPTY_SPACE)
or ((dest_x - delta_x == start_x) and (dest_y + 1 == start_y))
):
# we are moving up to a human checker that could jump us
quality -= 2
return MoveRecord(quality, start_x, start_y, dest_x, dest_y)
def remove_r_pieces(self, move_record):
self.remove_pieces(
move_record.start_x,
move_record.start_y,
move_record.dest_x,
move_record.dest_y,
)
def remove_pieces(self, start_x, start_y, dest_x, dest_y):
self.spaces[dest_x][dest_y] = self.spaces[start_x][start_y]
self.spaces[start_x][start_y] = EMPTY_SPACE
if abs(dest_x - start_x) == 2:
mid_x = (start_x + dest_x) // 2
mid_y = (start_y + dest_y) // 2
self.spaces[mid_x][mid_y] = EMPTY_SPACE
def play_computer_move(self, move_record):
print(
f"FROM {move_record.start_x} {move_record.start_y} TO {move_record.dest_x} {move_record.dest_y}"
)
while True:
if move_record.dest_y == BOTTOM_ROW:
# KING ME
self.remove_r_pieces(move_record)
self.spaces[move_record.dest_x][move_record.dest_y] = COMPUTER_KING
return
else:
self.spaces[move_record.dest_x][move_record.dest_y] = self.spaces[
move_record.start_x
][move_record.start_y]
self.remove_r_pieces(move_record)
if abs(move_record.dest_x - move_record.start_x) != 2:
return
landing_x = move_record.dest_x
landing_y = move_record.dest_y
best_move = None
if self.spaces[landing_x][landing_y] == COMPUTER_PIECE:
for delta_x in (-2, 2):
test_record = self.try_extend(landing_x, landing_y, delta_x, -2)
if not (move_record is None):
if (best_move is None) or (
move_record.quality > best_move.quality
):
best_move = test_record
else:
assert self.spaces[landing_x][landing_y] == COMPUTER_KING
for delta_x in (-2, 2):
for delta_y in (-2, 2):
test_record = self.try_extend(
landing_x, landing_y, delta_x, delta_y
)
if not (move_record is None):
if (best_move is None) or (
move_record.quality > best_move.quality
):
best_move = test_record
if best_move is None:
return
else:
print(f"TO {best_move.dest_x} {best_move.dest_y}")
move_record = best_move
def try_extend(self, start_x, start_y, delta_x, delta_y):
new_x = start_x + delta_x
new_y = start_y + delta_y
if not is_legal_board_coordinate(new_x, new_y):
return None
jumped_x = start_x + delta_x // 2
jumped_y = start_y + delta_y // 2
if (self.spaces[new_x][new_y] == EMPTY_SPACE) and (
self.spaces[jumped_x][jumped_y] > 0
):
return self.evaluate_move(start_x, start_y, new_x, new_y)
def get_human_move(self):
is_king = False
while True:
start_x, start_y = get_coordinates("FROM?")
if self.spaces[start_x][start_y] > 0:
break
is_king = self.spaces[start_x][start_y] == HUMAN_KING
while True:
dest_x, dest_y = get_coordinates("TO?")
if (not is_king) and (dest_y < start_y):
# CHEATER! Trying to move non-king backwards
continue
if (
(self.spaces[dest_x][dest_y] == 0)
and (abs(dest_x - start_x) <= 2)
and (abs(dest_x - start_x) == abs(dest_y - start_y))
):
break
return start_x, start_y, dest_x, dest_y
def get_human_extension(self, start_x, start_y):
is_king = self.spaces[start_x][start_y] == HUMAN_KING
while True:
dest_x, dest_y = get_coordinates("+TO?")
if dest_x < 0:
return False, None
if (not is_king) and (dest_y < start_y):
# CHEATER! Trying to move non-king backwards
continue
if (
(self.spaces[dest_x][dest_y] == EMPTY_SPACE)
and (abs(dest_x - start_x) == 2)
and (abs(dest_y - start_y) == 2)
):
return True, (start_x, start_y, dest_x, dest_y)
def play_human_move(self, start_x, start_y, dest_x, dest_y):
self.remove_pieces(start_x, start_y, dest_x, dest_y)
if dest_y == TOP_ROW:
# KING ME
self.spaces[dest_x][dest_y] = HUMAN_KING
def check_pieces(self):
if len(list(self.get_spaces_with_computer_pieces())) == 0:
print_human_won()
return False
if len(list(self.get_spaces_with_computer_pieces())) == 0:
print_computer_won()
return False
return True
def print_instructions():
print("THIS IS THE GAME OF CHECKERS. THE COMPUTER IS X,")
@@ -82,225 +349,6 @@ def print_instructions():
print()
def get_spaces():
for x in range(0, 8):
for y in range(0, 8):
yield x, y
def get_spaces_with_computer_pieces(board):
for x, y in get_spaces():
contents = board.spaces[x][y]
if contents < 0:
yield x, y
def get_spaces_with_human_pieces(board):
for x, y in get_spaces():
contents = board.spaces[x][y]
if contents > 0:
yield x, y
def pick_computer_move(board):
r = [INVALID_MOVE] * 5
for x, y in get_spaces_with_computer_pieces(board):
contents = board.spaces[x][y]
if contents == COMPUTER_PIECE:
for dx in (-1, 1):
dy = -1
sub_650(board, x, y, dx, dy, r)
else:
for dx in (-1, 1):
for dy in (-1, 1):
sub_650(board, x, y, dx, dy, r)
if r[0] != INVALID_MOVE:
dx = r[3] - r[1]
dy = r[4] - r[2]
if abs(dx) != abs(dy):
print(r)
assert abs(dx) == abs(dy)
return r
def sub_650(board, x, y, dx, dy, r):
new_x = x + dx
new_y = y + dy
if not ((0 <= new_x <= 7) and (0 <= new_y <= 7)):
return
contents = board.spaces[new_x][new_y]
if contents == 0:
sub_910(board, x, y, new_x, new_y, r)
return
if contents < 0:
return
# check landing space
landing_x = new_x + dx
landing_y = new_y + dy
# line 790
if not ((0 <= landing_x <= 7) and (0 <= landing_y <= 7)):
return
if board.spaces[landing_x][landing_y] == 0:
sub_910(board, x, y, landing_x, landing_y, r)
def sub_910(board, start_x, start_y, dest_x, dest_y, r):
q = 0
if dest_y == 0 and board.spaces[start_x][start_y] == COMPUTER_PIECE:
q += 2
if abs(start_y - dest_y) == 2:
q += 5
if start_y == 7:
q -= 2
if dest_x in (0, 7):
q += 1
for c in (-1, 1):
if (0 <= dest_x + c <= 7) and (1 <= dest_y):
# line 1035
if board.spaces[dest_x + c][dest_y - 1] < 0:
q += 1
# line 1040
elif (0 <= dest_x - c <= 7) and (dest_y + 1 <= 7):
# line 1045
if (
(board.spaces[dest_x + c][dest_y - 1] > 0)
and (board.spaces[dest_x - c][dest_y + 1] == EMPTY_SPACE)
or ((dest_x - c == start_x) and (dest_y + 1 == start_y))
):
q -= 2
# line 1080
if q > r[0]:
r[0] = q
r[1] = start_x
r[2] = start_y
r[3] = dest_x
r[4] = dest_y
def remove_r_pieces(board, r):
remove_pieces(board, r[1], r[2], r[3], r[4])
def remove_pieces(board, start_x, start_y, dest_x, dest_y):
board.spaces[dest_x][dest_y] = board.spaces[start_x][start_y]
board.spaces[start_x][start_y] = EMPTY_SPACE
if abs(dest_x - start_x) == 2:
mid_x = (start_x + dest_x) // 2
mid_y = (start_y + dest_y) // 2
board.spaces[mid_x][mid_y] = EMPTY_SPACE
def play_computer_move(board, r):
print(f"FROM {r[1]} {r[2]} TO {r[3]} {r[4]}")
while True:
if r[4] == 0:
# KING ME
board.spaces[r[3]][r[4]] = COMPUTER_KING
remove_r_pieces(board, r)
return
else:
# line 1250
board.spaces[r[3]][r[4]] = board.spaces[r[1]][r[2]]
remove_r_pieces(board, r)
if abs(r[1] - r[3]) != 2:
return
# line 1340
x = r[3]
y = r[4]
r[0] = INVALID_MOVE
if board.spaces[x][y] == COMPUTER_PIECE:
for a in (-2, 2):
try_extend(board, r, x, y, a, -2)
else:
assert board.spaces[x][y] == COMPUTER_KING
for a in (-2, 2):
for b in (-2, 2):
try_extend(board, r, x, y, a, b)
if r[0] != INVALID_MOVE:
print(f"TO {r[3]} {r[4]}")
else:
return
def try_extend(board, r, x, y, a, b):
# line 1370
nx = x + a
ny = y + b
if not ((0 <= nx <= 7) and (0 <= ny <= 7)):
return
if (board.spaces[nx][ny] == EMPTY_SPACE) and (
board.spaces[nx + a // 2][ny + b // 2] > 0
):
sub_910(board, x, y, nx, ny, r)
def get_human_move(board):
is_king = False
while True:
print("FROM?")
from_response = input()
x, y = [int(c) for c in from_response.split(",")]
if board.spaces[x][y] > 0:
break
is_king = board.spaces[x][y] == HUMAN_KING
while True:
print("TO?")
to_response = input()
a, b = [int(c) for c in to_response.split(",")]
if (not is_king) and (b < y):
# CHEATER! Trying to move non-king backwards
continue
if (
(board.spaces[a][b] == 0)
and (abs(a - x) <= 2)
and (abs(a - x) == abs(b - y))
):
break
return x, y, a, b
def get_human_extension(board, sx, sy):
is_king = board.spaces[sx][sy] == HUMAN_KING
while True:
print("+TO?")
to_response = input()
a1, b1 = [int(c) for c in to_response.split(",")]
if a1 < 0:
return False, None
if (not is_king) and (b1 < sy):
# CHEATER! Trying to move non-king backwards
continue
if (
(board.spaces[a1][b1] == EMPTY_SPACE)
and (abs(a1 - sx) == 2)
and (abs(b1 - sy) == 2)
):
return True, (sx, sy, a1, b1)
def play_human_move(board, start_x, start_y, dest_x, dest_y):
remove_pieces(board, start_x, start_y, dest_x, dest_y)
if dest_y == 7:
# KING ME
board.spaces[dest_x][dest_y] = HUMAN_KING
def print_human_won():
print()
print("YOU WIN.")
@@ -311,40 +359,30 @@ def print_computer_won():
print("I WIN.")
def check_pieces(board):
if len(list(get_spaces_with_computer_pieces(board))) == 0:
print_human_won()
return False
if len(list(get_spaces_with_computer_pieces(board))) == 0:
print_computer_won()
return False
return True
def play_game():
board = Board()
while True:
r = pick_computer_move(board)
if r[0] == INVALID_MOVE:
move_record = board.pick_computer_move()
if move_record is None:
print_human_won()
return
play_computer_move(board, r)
board.play_computer_move(move_record)
print(board)
if not check_pieces(board):
if not board.check_pieces():
return
sx, sy, dx, dy = get_human_move(board)
play_human_move(board, sx, sy, dx, dy)
if abs(dx - sx) == 2:
start_x, start_y, dest_x, dest_y = board.get_human_move()
board.play_human_move(start_x, start_y, dest_x, dest_y)
if abs(dest_x - start_x) == 2:
while True:
extend, move = get_human_extension(board, dx, dy)
extend, move = board.get_human_extension(dest_x, dest_y)
if not extend:
break
sx, sy, dx, dy = move
play_human_move(board, sx, sy, dx, dy)
start_x, start_y, dest_x, dest_y = move
board.play_human_move(start_x, start_y, dest_x, dest_y)
def main():