623 lines
19 KiB
Python
623 lines
19 KiB
Python
import utils
|
|
from typing import Union
|
|
|
|
Score = Union[int, float]
|
|
Move = tuple[tuple[int, int], tuple[int, int]]
|
|
Board = list[list[str]]
|
|
|
|
def evaluate(board: Board) -> Score:
|
|
"""
|
|
Returns the score of the current position.
|
|
|
|
Parameters
|
|
----------
|
|
board: 2D list of lists. Contains characters "B", "W", and "_",
|
|
representing black pawn, white pawn, and empty cell, respectively.
|
|
|
|
Returns
|
|
-------
|
|
An evaluation (as a Score).
|
|
"""
|
|
bcount = 0
|
|
wcount = 0
|
|
for r, row in enumerate(board):
|
|
for tile in row:
|
|
if tile == "B":
|
|
if r == 5:
|
|
return utils.WIN
|
|
bcount += 1
|
|
elif tile == "W":
|
|
if r == 0:
|
|
return -utils.WIN
|
|
wcount += 1
|
|
if wcount == 0:
|
|
return utils.WIN
|
|
if bcount == 0:
|
|
return -utils.WIN
|
|
return bcount - wcount
|
|
|
|
def generate_valid_moves(board: Board) -> list[Move]:
|
|
"""
|
|
Generates a list containing all possible moves in a particular position for black.
|
|
|
|
Parameters
|
|
----------
|
|
board: 2D list of lists. Contains characters "B", "W", and "_",
|
|
representing black pawn, white pawn, and empty cell, respectively.
|
|
|
|
Returns
|
|
-------
|
|
A list of Moves.
|
|
"""
|
|
res = []
|
|
for r, row in enumerate(board):
|
|
for c, tile in enumerate(row):
|
|
if tile != "B":
|
|
continue
|
|
for d in (-1, 0, 1):
|
|
dst = r + 1, c + d
|
|
if utils.is_valid_move(board, (r, c), dst):
|
|
res.append(((r, c), dst))
|
|
|
|
return res
|
|
|
|
def test_11():
|
|
board1 = [
|
|
["_", "_", "B", "_", "_", "_"],
|
|
["_", "_", "_", "_", "_", "_"],
|
|
["_", "_", "_", "_", "_", "_"],
|
|
["_", "_", "_", "_", "_", "_"],
|
|
["_", "_", "_", "_", "_", "_"],
|
|
["_", "_", "_", "_", "_", "_"]
|
|
]
|
|
assert sorted(generate_valid_moves(board1)) == [((0, 2), (1, 1)), ((0, 2), (1, 2)), ((0, 2), (1, 3))], "board1 test output is incorrect"
|
|
|
|
board2 = [
|
|
["_", "_", "_", "_", "B", "_"],
|
|
["_", "_", "_", "_", "_", "B"],
|
|
["_", "W", "_", "_", "_", "_"],
|
|
["_", "_", "W", "_", "_", "_"],
|
|
["_", "_", "_", "_", "_", "_"],
|
|
["_", "_", "B", "_", "_", "_"]
|
|
]
|
|
assert sorted(generate_valid_moves(board2)) == [((0, 4), (1, 3)), ((0, 4), (1, 4)), ((1, 5), (2, 4)), ((1, 5), (2, 5))], "board2 test output is incorrect"
|
|
|
|
board3 = [
|
|
["_", "_", "_", "_", "_", "_"],
|
|
["_", "_", "B", "_", "_", "_"],
|
|
["_", "W", "W", "W", "_", "_"],
|
|
["_", "_", "_", "_", "_", "_"],
|
|
["_", "_", "_", "_", "_", "_"],
|
|
["_", "_", "_", "_", "_", "_"]
|
|
]
|
|
assert sorted(generate_valid_moves(board3)) == [((1, 2), (2, 1)), ((1, 2), (2, 3))], "board3 test output is incorrect"
|
|
|
|
# test_11()
|
|
|
|
def minimax(
|
|
board: Board,
|
|
depth: int,
|
|
max_depth: int,
|
|
is_black: bool
|
|
) -> tuple[Score, Move]:
|
|
"""
|
|
Finds the best move for the input board state.
|
|
Note that you are black.
|
|
|
|
Parameters
|
|
----------
|
|
board: 2D list of lists. Contains characters "B", "W", and "_",
|
|
representing black pawn, white pawn, and empty cell, respectively.
|
|
|
|
depth: int, the depth to search for the best move. When this is equal
|
|
to `max_depth`, you should get the evaluation of the position using
|
|
the provided heuristic function.
|
|
|
|
max_depth: int, the maximum depth for cutoff.
|
|
|
|
is_black: bool. True when finding the best move for black, False
|
|
otherwise.
|
|
|
|
Returns
|
|
-------
|
|
A tuple (evalutation, ((src_row, src_col), (dst_row, dst_col))):
|
|
evaluation: the best score that black can achieve after this move.
|
|
src_row, src_col: position of the pawn to move.
|
|
dst_row, dst_col: position to move the pawn to.
|
|
"""
|
|
if depth == max_depth or utils.is_game_over(board):
|
|
return evaluate(board), None
|
|
# max value
|
|
if is_black:
|
|
v = -utils.INF
|
|
best_move = None
|
|
for action in generate_valid_moves(board):
|
|
next_board = utils.state_change(board, action[0], action[1], False)
|
|
new_v, _ = minimax(next_board, depth + 1, max_depth, False)
|
|
if new_v > v:
|
|
v = new_v
|
|
best_move = action
|
|
return v, best_move
|
|
# min value
|
|
else:
|
|
v = utils.INF
|
|
best_move = None
|
|
w_board = utils.invert_board(board, False)
|
|
for action in generate_valid_moves(w_board):
|
|
next_board = utils.state_change(w_board, action[0], action[1], False)
|
|
new_v, _ = minimax(utils.invert_board(next_board, False), depth + 1, max_depth, True)
|
|
if new_v < v:
|
|
v = new_v
|
|
best_move = action
|
|
return v, best_move
|
|
|
|
|
|
def test_21():
|
|
board1 = [
|
|
list("______"),
|
|
list("___B__"),
|
|
list("____BB"),
|
|
list("___WB_"),
|
|
list("_B__WW"),
|
|
list("_WW___"),
|
|
]
|
|
score1, _ = minimax(board1, 0, 1, True)
|
|
assert score1 == utils.WIN, "black should win in 1"
|
|
|
|
board2 = [
|
|
list("______"),
|
|
list("___B__"),
|
|
list("____BB"),
|
|
list("_BW_B_"),
|
|
list("____WW"),
|
|
list("_WW___"),
|
|
]
|
|
score2, _ = minimax(board2, 0, 3, True)
|
|
assert score2 == utils.WIN, "black should win in 3"
|
|
|
|
board3 = [
|
|
list("______"),
|
|
list("__B___"),
|
|
list("_WWW__"),
|
|
list("______"),
|
|
list("______"),
|
|
list("______"),
|
|
]
|
|
score3, _ = minimax(board3, 0, 4, True)
|
|
assert score3 == -utils.WIN, "white should win in 4"
|
|
|
|
# test_21()
|
|
|
|
def negamax(
|
|
board: Board,
|
|
depth: int,
|
|
max_depth: int
|
|
) -> tuple[Score, Move]:
|
|
"""
|
|
Finds the best move for the input board state.
|
|
Note that you are black.
|
|
|
|
Parameters
|
|
----------
|
|
board: 2D list of lists. Contains characters "B", "W", and "_",
|
|
representing black pawn, white pawn, and empty cell, respectively.
|
|
|
|
depth: int, the depth to search for the best move. When this is equal
|
|
to `max_depth`, you should get the evaluation of the position using
|
|
the provided heuristic function.
|
|
|
|
max_depth: int, the maximum depth for cutoff.
|
|
|
|
Notice that you no longer need the parameter `is_black`.
|
|
|
|
Returns
|
|
-------
|
|
A tuple (evalutation, ((src_row, src_col), (dst_row, dst_col))):
|
|
evaluation: the best score that black can achieve after this move.
|
|
src_row, src_col: position of the pawn to move.
|
|
dst_row, dst_col: position to move the pawn to.
|
|
"""
|
|
if depth == max_depth or utils.is_game_over(board):
|
|
return evaluate(board), None
|
|
v = -utils.INF
|
|
best_move = None
|
|
for action in generate_valid_moves(board):
|
|
# if current is black, then the next board is white. It is inverted after applying the black move
|
|
next_board = utils.invert_board(utils.state_change(board, action[0], action[1], False), False)
|
|
# since this move is white, the score needs to be negated (to get the min value of the white move)
|
|
new_v, _ = negamax(next_board, depth + 1, max_depth)
|
|
# if the negated score is greater than the current max value, update the max value and the best move
|
|
if -new_v > v:
|
|
v = -new_v
|
|
best_move = action
|
|
return v, best_move
|
|
|
|
def test_22():
|
|
board1 = [
|
|
list("______"),
|
|
list("___B__"),
|
|
list("____BB"),
|
|
list("___WB_"),
|
|
list("_B__WW"),
|
|
list("_WW___"),
|
|
]
|
|
score1, _ = negamax(board1, 0, 1)
|
|
assert score1 == utils.WIN, "black should win in 1"
|
|
|
|
board2 = [
|
|
list("______"),
|
|
list("___B__"),
|
|
list("____BB"),
|
|
list("_BW_B_"),
|
|
list("____WW"),
|
|
list("_WW___"),
|
|
]
|
|
score2, _ = negamax(board2, 0, 3)
|
|
assert score2 == utils.WIN, "black should win in 3"
|
|
|
|
board3 = [
|
|
list("______"),
|
|
list("__B___"),
|
|
list("_WWW__"),
|
|
list("______"),
|
|
list("______"),
|
|
list("______"),
|
|
]
|
|
score3, _ = negamax(board3, 0, 4)
|
|
assert score3 == -utils.WIN, "white should win in 4"
|
|
|
|
# test_22()
|
|
|
|
def minimax_alpha_beta(
|
|
board: Board,
|
|
depth: int,
|
|
max_depth: int,
|
|
alpha: Score,
|
|
beta: Score,
|
|
is_black: bool
|
|
) -> tuple[Score, Move]:
|
|
"""
|
|
Finds the best move for the input board state.
|
|
Note that you are black.
|
|
|
|
Parameters
|
|
----------
|
|
board: 2D list of lists. Contains characters "B", "W", and "_",
|
|
representing black pawn, white pawn, and empty cell, respectively.
|
|
|
|
depth: int, the depth to search for the best move. When this is equal
|
|
to `max_depth`, you should get the evaluation of the position using
|
|
the provided heuristic function.
|
|
|
|
max_depth: int, the maximum depth for cutoff.
|
|
|
|
alpha: Score. The alpha value in a given state.
|
|
|
|
beta: Score. The beta value in a given state.
|
|
|
|
is_black: bool. True when finding the best move for black, False
|
|
otherwise.
|
|
|
|
Returns
|
|
-------
|
|
A tuple (evalutation, ((src_row, src_col), (dst_row, dst_col))):
|
|
evaluation: the best score that black can achieve after this move.
|
|
src_row, src_col: position of the pawn to move.
|
|
dst_row, dst_col: position to move the pawn to.
|
|
"""
|
|
# base case
|
|
if depth == max_depth or utils.is_game_over(board):
|
|
return evaluate(board), None
|
|
|
|
# max value
|
|
if is_black:
|
|
v = -utils.INF
|
|
best_move = None
|
|
for action in generate_valid_moves(board):
|
|
next_board = utils.state_change(board, action[0], action[1], False)
|
|
new_v, _ = minimax_alpha_beta(next_board, depth + 1, max_depth, alpha, beta, False)
|
|
if new_v > v:
|
|
v = new_v
|
|
best_move = action
|
|
if v >= beta:
|
|
break
|
|
alpha = max(alpha, v)
|
|
return v, best_move
|
|
# min value
|
|
else:
|
|
v = utils.INF
|
|
best_move = None
|
|
w_board = utils.invert_board(board, False)
|
|
for action in generate_valid_moves(w_board):
|
|
next_board = utils.invert_board(utils.state_change(w_board, action[0], action[1], False), False)
|
|
new_v, _ = minimax_alpha_beta(next_board, depth + 1, max_depth, alpha, beta, True)
|
|
if new_v < v:
|
|
v = new_v
|
|
best_move = action
|
|
if v <= alpha:
|
|
break
|
|
beta = min(beta, v)
|
|
return v, best_move
|
|
|
|
def test_31():
|
|
board1 = [
|
|
list("______"),
|
|
list("__BB__"),
|
|
list("____BB"),
|
|
list("WBW_B_"),
|
|
list("____WW"),
|
|
list("_WW___"),
|
|
]
|
|
score1, _ = minimax_alpha_beta(board1, 0, 3, -utils.INF, utils.INF, True)
|
|
assert score1 == utils.WIN, "black should win in 3"
|
|
|
|
|
|
board2 = [
|
|
list("____B_"),
|
|
list("___B__"),
|
|
list("__B___"),
|
|
list("_WWW__"),
|
|
list("____W_"),
|
|
list("______"),
|
|
]
|
|
score2, _ = minimax_alpha_beta(board2, 0, 5, -utils.INF, utils.INF, True)
|
|
assert score2 == utils.WIN, "black should win in 5"
|
|
|
|
board3 = [
|
|
list("____B_"),
|
|
list("__BB__"),
|
|
list("______"),
|
|
list("_WWW__"),
|
|
list("____W_"),
|
|
list("______"),
|
|
]
|
|
score3, m = minimax_alpha_beta(board3, 0, 6, -utils.INF, utils.INF, True)
|
|
print(m)
|
|
assert score3 == -utils.WIN, "white should win in 6"
|
|
|
|
# print("custom")
|
|
# print(minimax_alpha_beta([['_', '_', '_', '_', 'B', '_'], ['_', '_', 'B', 'B', '_', '_'], ['_', '_', '_', '_', '_', '_'], ['_', 'W', 'W', 'W', '_', '_'], ['_', '_', '_', '_', 'W', '_'], ['_', '_', '_', '_', '_', '_']],
|
|
# 0, 6, -utils.INF, utils.INF, True))
|
|
# print("custom end")
|
|
|
|
def negamax_alpha_beta(
|
|
board: Board,
|
|
depth: int,
|
|
max_depth: int,
|
|
alpha: Score,
|
|
beta: Score
|
|
) -> tuple[Score, Move]:
|
|
"""
|
|
Finds the best move for the input board state.
|
|
Note that you are black.
|
|
|
|
Parameters
|
|
----------
|
|
board: 2D list of lists. Contains characters "B", "W", and "_",
|
|
representing black pawn, white pawn, and empty cell, respectively.
|
|
|
|
depth: int, the depth to search for the best move. When this is equal
|
|
to `max_depth`, you should get the evaluation of the position using
|
|
the provided heuristic function.
|
|
|
|
max_depth: int, the maximum depth for cutoff.
|
|
|
|
alpha: Score. The alpha value in a given state.
|
|
|
|
beta: Score. The beta value in a given state.
|
|
|
|
Notice that you no longer need the parameter `is_black`.
|
|
|
|
Returns
|
|
-------
|
|
A tuple (evalutation, ((src_row, src_col), (dst_row, dst_col))):
|
|
evaluation: the best score that black can achieve after this move.
|
|
src_row, src_col: position of the pawn to move.
|
|
dst_row, dst_col: position to move the pawn to.
|
|
"""
|
|
if depth == max_depth or utils.is_game_over(board):
|
|
return evaluate(board), None
|
|
best_move = None
|
|
for action in generate_valid_moves(board):
|
|
# if current is black, then the next board is white. It is inverted after applying the black move
|
|
next_board = utils.invert_board(utils.state_change(board, action[0], action[1], False), False)
|
|
# since this move is white, the score needs to be negated (to get the min value of the white move)
|
|
new_v, _ = negamax_alpha_beta(next_board, depth + 1, max_depth, -beta, -alpha)
|
|
# if the negated score is greater than the current max value, update the max value and the best move
|
|
if -new_v > alpha:
|
|
alpha = -new_v
|
|
best_move = action
|
|
if alpha >= beta:
|
|
break
|
|
return alpha, best_move
|
|
|
|
def test_32():
|
|
board1 = [
|
|
list("______"),
|
|
list("__BB__"),
|
|
list("____BB"),
|
|
list("WBW_B_"),
|
|
list("____WW"),
|
|
list("_WW___"),
|
|
]
|
|
score1, _ = negamax_alpha_beta(board1, 0, 3, -utils.INF, utils.INF)
|
|
assert score1 == utils.WIN, "black should win in 3"
|
|
|
|
board2 = [
|
|
list("____B_"),
|
|
list("___B__"),
|
|
list("__B___"),
|
|
list("_WWW__"),
|
|
list("____W_"),
|
|
list("______"),
|
|
]
|
|
score2, _ = negamax_alpha_beta(board2, 0, 5, -utils.INF, utils.INF)
|
|
assert score2 == utils.WIN, "black should win in 5"
|
|
|
|
board3 = [
|
|
list("____B_"),
|
|
list("__BB__"),
|
|
list("______"),
|
|
list("_WWW__"),
|
|
list("____W_"),
|
|
list("______"),
|
|
]
|
|
score3, _ = negamax_alpha_beta(board3, 0, 6, -utils.INF, utils.INF)
|
|
assert score3 == -utils.WIN, "white should win in 6"
|
|
|
|
# test_32()
|
|
|
|
# Uncomment and implement the function.
|
|
# Note: this will override the provided `evaluate` function.
|
|
|
|
def evaluate(board: Board) -> Score:
|
|
score = 0
|
|
bcount = 0
|
|
wcount = 0
|
|
for r, row in enumerate(board):
|
|
for tile in row:
|
|
if tile == "B":
|
|
bcount += 1
|
|
if r == 0:
|
|
score += 10
|
|
if r == 1:
|
|
score += 10
|
|
if r == 2:
|
|
score += 20
|
|
if r == 3:
|
|
score += 40
|
|
if r == 4:
|
|
score += 50
|
|
if r == 5:
|
|
return utils.WIN
|
|
elif tile == "W":
|
|
wcount += 1
|
|
if r == 0:
|
|
return -utils.WIN
|
|
if r == 1:
|
|
score -= 50
|
|
if r == 2:
|
|
score -= 40
|
|
if r == 3:
|
|
score -= 20
|
|
if r == 4:
|
|
score -= 10
|
|
if r == 5:
|
|
score -= 10
|
|
if wcount == 0:
|
|
return utils.WIN
|
|
if bcount == 0:
|
|
return -utils.WIN
|
|
return score
|
|
|
|
def test_41():
|
|
board1 = [
|
|
["_", "_", "_", "B", "_", "_"],
|
|
["_", "_", "_", "W", "_", "_"],
|
|
["_", "_", "_", "_", "_", "_"],
|
|
["_", "_", "B", "_", "_", "_"],
|
|
["_", "_", "_", "_", "_", "_"],
|
|
["_", "_", "_", "_", "_", "_"]
|
|
]
|
|
assert evaluate(board1) == 0
|
|
|
|
board2 = [
|
|
["_", "_", "_", "B", "W", "_"],
|
|
["_", "_", "_", "W", "_", "_"],
|
|
["_", "_", "_", "_", "_", "_"],
|
|
["_", "_", "_", "_", "_", "_"],
|
|
["_", "_", "_", "_", "_", "_"],
|
|
["_", "_", "_", "_", "_", "_"]
|
|
]
|
|
assert evaluate(board2) == -utils.WIN
|
|
|
|
board3 = [
|
|
["_", "_", "_", "_", "_", "_"],
|
|
["_", "_", "_", "_", "_", "_"],
|
|
["_", "_", "_", "_", "_", "_"],
|
|
["_", "_", "B", "_", "_", "_"],
|
|
["_", "_", "_", "_", "_", "_"],
|
|
["_", "_", "_", "_", "_", "_"]
|
|
]
|
|
assert evaluate(board3) == utils.WIN
|
|
|
|
test_41()
|
|
|
|
class PlayerAI:
|
|
|
|
def make_move(self, board: Board) -> Move:
|
|
"""
|
|
This is the function that will be called from main.py
|
|
You should combine the functions in the previous tasks
|
|
to implement this function.
|
|
|
|
Parameters
|
|
----------
|
|
self: object instance itself, passed in automatically by Python.
|
|
|
|
board: 2D list-of-lists. Contains characters "B", "W", and "_",
|
|
representing black pawn, white pawn, and empty cell, respectively.
|
|
|
|
Returns
|
|
-------
|
|
A tuple of tuples containing coordinates (row_index, col_index).
|
|
The first tuple contains the source position of the black pawn
|
|
to be moved, the second tuple contains the destination position.
|
|
"""
|
|
# TODO: Replace starter code with your AI
|
|
################
|
|
# Starter code #
|
|
################
|
|
for r in range(len(board)):
|
|
for c in range(len(board[r])):
|
|
# check if B can move forward directly
|
|
if board[r][c] == "B" and board[r + 1][c] == "_":
|
|
src = r, c
|
|
dst = r + 1, c
|
|
return src, dst # valid move
|
|
return (0, 0), (0, 0) # invalid move
|
|
|
|
class PlayerNaive:
|
|
"""
|
|
A naive agent that will always return the first available valid move.
|
|
"""
|
|
def make_move(self, board: Board) -> Move:
|
|
return utils.generate_rand_move(board)
|
|
|
|
##########################
|
|
# Game playing framework #
|
|
##########################
|
|
if __name__ == "__main__":
|
|
assert utils.test_move([
|
|
["B", "B", "B", "B", "B", "B"],
|
|
["_", "B", "B", "B", "B", "B"],
|
|
["_", "_", "_", "_", "_", "_"],
|
|
["_", "B", "_", "_", "_", "_"],
|
|
["_", "W", "W", "W", "W", "W"],
|
|
["W", "W", "W", "W", "W", "W"]
|
|
], PlayerAI())
|
|
|
|
assert utils.test_move([
|
|
["_", "B", "B", "B", "B", "B"],
|
|
["_", "B", "B", "B", "B", "B"],
|
|
["_", "_", "_", "_", "_", "_"],
|
|
["_", "B", "_", "_", "_", "_"],
|
|
["W", "W", "W", "W", "W", "W"],
|
|
["_", "_", "W", "W", "W", "W"]
|
|
], PlayerAI())
|
|
|
|
assert utils.test_move([
|
|
["_", "_", "B", "B", "B", "B"],
|
|
["_", "B", "B", "B", "B", "B"],
|
|
["_", "_", "_", "_", "_", "_"],
|
|
["_", "B", "W", "_", "_", "_"],
|
|
["_", "W", "W", "W", "W", "W"],
|
|
["_", "_", "_", "W", "W", "W"]
|
|
], PlayerAI())
|
|
|
|
# generates initial board
|
|
# board = utils.generate_init_state()
|
|
# res = utils.play(PlayerAI(), PlayerNaive(), board)
|
|
# # Black wins means your agent wins.
|
|
# print(res)
|