mirror of
https://github.com/coding-horror/basic-computer-games.git
synced 2025-12-24 20:10:15 -08:00
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:
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user