154 lines
6.2 KiB
Python
154 lines
6.2 KiB
Python
"""CSC111 Winter 2021 Assignment 2: Trees, Chess, and Artificial Intelligence (Part 3)
|
|
|
|
Instructions (READ THIS FIRST!)
|
|
===============================
|
|
|
|
This Python module contains the start of functions and/or classes you'll define
|
|
for Part 3 of this assignment. You should NOT make any changes to a2_minichess.py.
|
|
|
|
Copyright and Usage Information
|
|
===============================
|
|
|
|
This file is provided solely for the personal and private use of students
|
|
taking CSC111 at the University of Toronto St. George campus. All forms of
|
|
distribution of this code, whether as given or with any changes, are
|
|
expressly prohibited. For more information on copyright for CSC111 materials,
|
|
please consult our Course Syllabus.
|
|
|
|
This file is Copyright (c) 2022 Mario Badr, David Liu, and Isaac Waller.
|
|
"""
|
|
import random
|
|
from typing import Optional
|
|
|
|
import a2_game_tree
|
|
import a2_minichess
|
|
|
|
|
|
class ExploringPlayer(a2_minichess.Player):
|
|
"""A Minichess player that plays greedily some of the time, and randomly some of the time.
|
|
|
|
See assignment handout for details.
|
|
"""
|
|
# Private Instance Attributes:
|
|
# - _game_tree:
|
|
# The GameTree that this player uses to make its moves. If None, then this
|
|
# player just makes random moves.
|
|
_game_tree: Optional[a2_game_tree.GameTree]
|
|
_exploration_probability: float
|
|
|
|
def __init__(self, game_tree: a2_game_tree.GameTree, exploration_probability: float) -> None:
|
|
"""Initialize this player."""
|
|
self._game_tree = game_tree
|
|
self._exploration_probability = exploration_probability
|
|
|
|
def make_move(self, game: a2_minichess.MinichessGame, previous_move: Optional[str]) -> str:
|
|
"""Make a move given the current game.
|
|
|
|
previous_move is the opponent player's most recent move, or None if no moves
|
|
have been made.
|
|
|
|
Preconditions:
|
|
- There is at least one valid move for the given game
|
|
"""
|
|
if self._game_tree and previous_move:
|
|
self._game_tree = self._game_tree.find_subtree_by_move(previous_move)
|
|
|
|
# Tree has been explored, make a random choice
|
|
if self._game_tree is None or len(self._game_tree.get_subtrees()) == 0 :
|
|
move = random.choice(game.get_valid_moves())
|
|
|
|
# "If the random number is < p, the player chooses a random move from among all valid moves
|
|
# for the current game state"
|
|
elif random.random() < self._exploration_probability:
|
|
move = random.choice(game.get_valid_moves())
|
|
|
|
# The player picks its move
|
|
else:
|
|
subtrees = self._game_tree.get_subtrees()
|
|
if game.is_white_move():
|
|
move = max(subtrees, key=lambda x: x.white_win_probability).move
|
|
else:
|
|
move = min(subtrees, key=lambda x: x.white_win_probability).move
|
|
|
|
# Update tree
|
|
if self._game_tree:
|
|
self._game_tree = self._game_tree.find_subtree_by_move(move)
|
|
|
|
return move
|
|
|
|
|
|
def run_learning_algorithm(exploration_probabilities: list[float],
|
|
show_stats: bool = True) -> a2_game_tree.GameTree:
|
|
"""Play a sequence of Minichess games using an ExploringPlayer as the White player.
|
|
|
|
This algorithm first initializes an empty GameTree. All ExploringPlayers will use this
|
|
SAME GameTree object, which will be mutated over the course of the algorithm!
|
|
Return this object.
|
|
|
|
There are len(exploration_probabilities) games played, where at game i (starting at 0):
|
|
- White is an ExploringPlayer (using the game tree) whose exploration probability
|
|
is equal to exploration_probabilities[i]
|
|
- Black is a RandomPlayer
|
|
- AFTER the game, the move sequence from the game is inserted into the game tree,
|
|
with a white win probability of 1.0 if White won the game, and 0.0 otherwise.
|
|
|
|
Implementation note:
|
|
- A NEW ExploringPlayer instance should be created for each loop iteration.
|
|
However, each one should use the SAME GameTree object.
|
|
- You should call run_game, NOT run_games, from a2_minichess. This is because you
|
|
need more control over what happens after each game runs, which you can get by
|
|
writing your own loop that calls run_game. However, you can base your loop on
|
|
the implementation of run_games.
|
|
- Note that run_game from a2_minichess returns both the winner and the move sequence
|
|
after the game ends.
|
|
- You may call print in this function to report progress made in each game.
|
|
- Note that this function returns the final GameTree object. You can inspect the
|
|
white_win_probability of its nodes, calculate its size, or and use it in a
|
|
RandomTreePlayer or GreedyTreePlayer to see how they do with it.
|
|
"""
|
|
# Start with a GameTree in the initial state
|
|
game_tree = a2_game_tree.GameTree()
|
|
|
|
# Play games using the ExploringPlayer and update the GameTree after each one
|
|
results_so_far = []
|
|
|
|
# Loop through each probability, play games
|
|
for exp_prob in exploration_probabilities:
|
|
# Run game
|
|
winner, seq = a2_minichess.run_game(ExploringPlayer(game_tree, exp_prob),
|
|
a2_minichess.RandomPlayer())
|
|
# After the game, insert move sequence with white win probability
|
|
game_tree.insert_move_sequence(seq, float(winner == 'White'))
|
|
# Print progress
|
|
print(f'Winner: {winner}')
|
|
|
|
if show_stats:
|
|
a2_minichess.plot_game_statistics(results_so_far)
|
|
|
|
return game_tree
|
|
|
|
|
|
def part3_runner() -> a2_game_tree.GameTree:
|
|
"""Run example for Part 3.
|
|
|
|
Please note that unlike part1_runner and part2_runner, this function is NOT graded.
|
|
We encourage you to experiment with different exploration probability sequences
|
|
to see how quickly you can develop a "winning" GameTree!
|
|
"""
|
|
probabilities = [0.0] * 700
|
|
|
|
return run_learning_algorithm(probabilities)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
import python_ta
|
|
python_ta.check_all(config={
|
|
'max-line-length': 100,
|
|
'max-nested-blocks': 4,
|
|
'disable': ['E1136'],
|
|
'extra-imports': ['random', 'a2_minichess', 'a2_game_tree'],
|
|
'allowed-io': ['run_learning_algorithm']
|
|
})
|
|
|
|
part3_runner()
|