From 7ddb802f3720e5e53dfc2c64cd5fe66f3fdb8e46 Mon Sep 17 00:00:00 2001 From: "github-classroom[bot]" <66690702+github-classroom[bot]@users.noreply.github.com> Date: Thu, 10 Mar 2022 15:24:03 +0000 Subject: [PATCH 01/25] Setting up GitHub Classroom Feedback From b5a3fd2c38aecf54eaf452144aab6063eb6c4e4a Mon Sep 17 00:00:00 2001 From: cWetaski Date: Wed, 30 Mar 2022 16:23:24 -0400 Subject: [PATCH 02/25] notes from meeting --- Strategizing_Notes.txt | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 Strategizing_Notes.txt diff --git a/Strategizing_Notes.txt b/Strategizing_Notes.txt new file mode 100644 index 0000000..b5d1c79 --- /dev/null +++ b/Strategizing_Notes.txt @@ -0,0 +1,13 @@ +Implementation stuff +- determine available moves +- evaluate whether a position/move ends the game and also evaluate which player wins in such a state + - game ends when there is no path from player to player + - compute how many distinct paths there are from 1 player to another? + - how many walls would be required to block the path from A to B + - would this move allow the game to be ended and who would win? +Strategy +- never end turn with 3 walls arouund you if opponent is fewer than M steps away +- randomly move initially (or just use simple heuristic), then search when there are fewer moves available +- alpha beta pruning -> in a certain state, if a certain player can win with a move, there is no need to consider other moves +- herusitic: minimum number of walls to enclose the player +- determine conditions for switching from simple heuristic to alpha-beta search From ddaf807a7a7c69b3be63677ee2371bd3cb1d56af Mon Sep 17 00:00:00 2001 From: cWetaski Date: Wed, 30 Mar 2022 16:25:36 -0400 Subject: [PATCH 03/25] editted notes --- Strategizing_Notes.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/Strategizing_Notes.txt b/Strategizing_Notes.txt index b5d1c79..12aae9b 100644 --- a/Strategizing_Notes.txt +++ b/Strategizing_Notes.txt @@ -11,3 +11,4 @@ Strategy - alpha beta pruning -> in a certain state, if a certain player can win with a move, there is no need to consider other moves - herusitic: minimum number of walls to enclose the player - determine conditions for switching from simple heuristic to alpha-beta search +TEST From 0e98e9c6350fd6609ce2c36250391e4efb46e3e4 Mon Sep 17 00:00:00 2001 From: cWetaski Date: Thu, 31 Mar 2022 16:20:04 -0400 Subject: [PATCH 04/25] added gameplayer.py, test_agent.py --- agents/__init__.py | 1 + agents/test_agent.py | 61 ++++++++++++++++++++++++++++++++++++++++++++ gameplayer.py | 11 ++++++++ 3 files changed, 73 insertions(+) create mode 100644 agents/test_agent.py create mode 100644 gameplayer.py diff --git a/agents/__init__.py b/agents/__init__.py index 5ee095a..bcdb77c 100644 --- a/agents/__init__.py +++ b/agents/__init__.py @@ -2,3 +2,4 @@ from .random_agent import RandomAgent from .human_agent import HumanAgent from .student_agent import StudentAgent +from .test_agent import TestAgent diff --git a/agents/test_agent.py b/agents/test_agent.py new file mode 100644 index 0000000..47bb035 --- /dev/null +++ b/agents/test_agent.py @@ -0,0 +1,61 @@ +# Student agent: Add your own agent here +from agents.agent import Agent +from store import register_agent +import sys + + +@register_agent("test_agent") +class TestAgent(Agent): + + def __init__(self): + super(TestAgent, self).__init__() + self.name = "TestAgent" + self.dir_map = { + "u": 0, + "r": 1, + "d": 2, + "l": 3, + } + + def step(self, chess_board, my_pos, adv_pos, max_step): + self.get_valid_moves(chess_board, my_pos, max_step) + text = input("Your move (x,y,dir) or input q to quit: ") + + while len(text.split(",")) != 3 and "q" not in text.lower(): + print("Wrong Input Format!") + text = input("Your move (x,y,dir) or input q to quit: ") + if "q" in text.lower(): + print("Game ended by user!") + sys.exit(0) + x, y, dir = text.split(",") + x, y, dir = x.strip(), y.strip(), dir.strip() + x, y = int(x), int(y) + while not self.check_valid_input( + x, y, dir, chess_board.shape[0], chess_board.shape[1] + ): + print( + "Invalid Move! (x, y) should be within the board and dir should be one of u,r,d,l." + ) + text = input("Your move (x,y,dir) or input q to quit: ") + while len(text.split(",")) != 3 and "q" not in text.lower(): + print("Wrong Input Format!") + text = input("Your move (x,y,dir) or input q to quit: ") + if "q" in text.lower(): + print("Game ended by user!") + sys.exit(0) + x, y, dir = text.split(",") + x, y, dir = x.strip(), y.strip(), dir.strip() + x, y = int(x), int(y) + my_pos = (x, y) + return my_pos, self.dir_map[dir] + + def check_valid_input(self, x, y, dir, x_max, y_max): + return 0 <= x < x_max and 0 <= y < y_max and dir in self.dir_map + + def get_valid_moves(self, chess_board, my_pos, max_step): + print(my_pos) + print(chess_board.shape) + + + + diff --git a/gameplayer.py b/gameplayer.py new file mode 100644 index 0000000..fee2d11 --- /dev/null +++ b/gameplayer.py @@ -0,0 +1,11 @@ +import simulator + +args = simulator.get_args() +args.player_1 = "test_agent" +args.player_2 = "random_agent" +s1 = simulator.Simulator(args) +result = s1.run() + + + + From ee18ec6492b907e109272d57274266372acaf250 Mon Sep 17 00:00:00 2001 From: cWetaski Date: Thu, 31 Mar 2022 17:35:53 -0400 Subject: [PATCH 05/25] had unsaved files --- agents/test_agent.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/agents/test_agent.py b/agents/test_agent.py index 47bb035..cf10945 100644 --- a/agents/test_agent.py +++ b/agents/test_agent.py @@ -54,6 +54,12 @@ def check_valid_input(self, x, y, dir, x_max, y_max): def get_valid_moves(self, chess_board, my_pos, max_step): print(my_pos) + board_size = chess_board.shape[0] + num_steps = max_step + while (num_steps > 0) + + + print(chess_board.shape) From b59ae83249c678b7b5c1d39f514148819e3738b1 Mon Sep 17 00:00:00 2001 From: FFFlora0349 <59624826+FFFlora0349@users.noreply.github.com> Date: Sun, 3 Apr 2022 18:40:06 -0400 Subject: [PATCH 06/25] step() fct initial version --- Strategizing_Notes.txt | 12 +++-- agents/search_tree.py | 9 ++++ agents/student_agent.py | 80 ++++++++++++++++++++++++++++++-- agents/student_agent_template.py | 40 ++++++++++++++++ agents/test_agent.py | 6 ++- simulator.py | 1 + 6 files changed, 140 insertions(+), 8 deletions(-) create mode 100644 agents/search_tree.py create mode 100644 agents/student_agent_template.py diff --git a/Strategizing_Notes.txt b/Strategizing_Notes.txt index 12aae9b..70e8b91 100644 --- a/Strategizing_Notes.txt +++ b/Strategizing_Notes.txt @@ -1,14 +1,20 @@ Implementation stuff - determine available moves - evaluate whether a position/move ends the game and also evaluate which player wins in such a state +(already provided in check_endgame() in world.py) - game ends when there is no path from player to player - compute how many distinct paths there are from 1 player to another? - how many walls would be required to block the path from A to B - would this move allow the game to be ended and who would win? Strategy -- never end turn with 3 walls arouund you if opponent is fewer than M steps away +- never end turn with 3 walls around you if opponent is fewer than M steps away - randomly move initially (or just use simple heuristic), then search when there are fewer moves available - alpha beta pruning -> in a certain state, if a certain player can win with a move, there is no need to consider other moves -- herusitic: minimum number of walls to enclose the player +- heursitic: minimum number of walls to enclose the player - determine conditions for switching from simple heuristic to alpha-beta search -TEST + +UPDATE: +We want to reduce the branching factor of the search tree but still cover each possible distance. +eg. for a max_step equal to 5, we search some move of distance 1, some move of distance 2, some of distance 3, 4, and 5. + +To implement the minimax algo... diff --git a/agents/search_tree.py b/agents/search_tree.py new file mode 100644 index 0000000..950cbb9 --- /dev/null +++ b/agents/search_tree.py @@ -0,0 +1,9 @@ +# Never mind this... +class Tree: + def __init__(self): + self.root = None + + class Node: + def __init__(self, game_state): + self.children = [] + self.data = game_state diff --git a/agents/student_agent.py b/agents/student_agent.py index 36b9509..dacf30a 100644 --- a/agents/student_agent.py +++ b/agents/student_agent.py @@ -1,8 +1,11 @@ # Student agent: Add your own agent here +from copy import deepcopy from agents.agent import Agent from store import register_agent import sys +import numpy as np + @register_agent("student_agent") class StudentAgent(Agent): @@ -25,7 +28,7 @@ def step(self, chess_board, my_pos, adv_pos, max_step): """ Implement the step function of your agent here. You can use the following variables to access the chess board: - - chess_board: a numpy array of shape (x_max, y_max, 4) + - chess_board: a numpy array of shape (x_max, y_max, 4) 3-dimentional - my_pos: a tuple of (x, y) - adv_pos: a tuple of (x, y) - max_step: an integer @@ -36,5 +39,76 @@ def step(self, chess_board, my_pos, adv_pos, max_step): Please check the sample implementation in agents/random_agent.py or agents/human_agent.py for more details. """ - # dummy return - return my_pos, self.dir_map["u"] + moves = ((-1, 0), (0, 1), (1, 0), (0, -1)) + + best_score = 0 + best_move = [(-1,-1), -1] + + my_new_pos = (-1,-1) + dir_wall = -1 + + # Stores all previous moves that we have searched + previous_moves = [(-1,-1), -1] + + for step_size in range(1, max_step+1) # eg. when moving 4 steps + for _ in range (0, 2) # we make this number of different moves for our agent in this step_size + # eg. we take 2 different 4-step moves of the agent as a possibility to search + for each_step in range (1, step_size+1) # move to each square... + + # decide a direction to move in + dir_move = np.random.randint(0, 4) + + # we place a wall only on the last step: + if each_step == step_size: + dir_wall = np.random.randint(0,4) + + while not self.world.check_valid_step(my_pos, my_pos + moves[dir_move], dir_wall): + + # generate a valid next step: + dir_move = np.random.randint(0,4) + + # we place a wall only on the last step: + if each_step == step_size: + dir_wall = np.random.randint(0,4) + + # If we have already checked this move, do new move... (back to while) + if (dir_move, dir_wall) in previous_moves: + dir_move = (-1,-1) + dir_wall = -1 + + my_new_pos = my_pos + moves[dir_move] + r, c = my_new_pos + if each_step == step_size: + self.world.set_barrier(r, c, dir_wall) # ! and to unset_barrier at the same position later? + # a new move of step-size steps has been generated here--- + + # Run the minimax algo to check where to place our agent is the best + score = minimax(chess_board, my_new_pos, adv_pos, False, 10) + + # Get the optimal ? position and wall direction + r, c = my_new_pos + self.world.unset_barrier(r, c, dir_wall) + if score > best_score: + best_score = score + best_move = my_new_pos + best_dir_wall = dir_wall + + return best_move, best_dir_wall + + + def minimax(chess_board, my_new_pos, adv_pos, is_maximizing, depth): + """ + is_maximizing is a bool indicating whether we are at maximizing or minimizing step + depth is the depth of search + """ + + return false + # M x M board. max_step = floor((M+1)/2) + # for each node in the search tree, it has children for every possible step number : + # some children after moving 1 step, moving 2 steps, ... 5 steps max. + # to reduce the branching factor of the search tree but still cover each possible distance, + # we can calculate only 2 positions for each number <= max_step, and 4 positions for the wall. + + + + \ No newline at end of file diff --git a/agents/student_agent_template.py b/agents/student_agent_template.py new file mode 100644 index 0000000..5780ea2 --- /dev/null +++ b/agents/student_agent_template.py @@ -0,0 +1,40 @@ +# Student agent: Add your own agent here +from agents.agent import Agent +from store import register_agent +import sys + + +@register_agent("student_agent") +class StudentAgent(Agent): + """ + A dummy class for your implementation. Feel free to use this class to + add any helper functionalities needed for your agent. + """ + + def __init__(self): + super(StudentAgent, self).__init__() + self.name = "StudentAgent" + self.dir_map = { + "u": 0, + "r": 1, + "d": 2, + "l": 3, + } + + def step(self, chess_board, my_pos, adv_pos, max_step): + """ + Implement the step function of your agent here. + You can use the following variables to access the chess board: + - chess_board: a numpy array of shape (x_max, y_max, 4) 3-dimentional + - my_pos: a tuple of (x, y) + - adv_pos: a tuple of (x, y) + - max_step: an integer + + You should return a tuple of ((x, y), dir), + where (x, y) is the next position of your agent and dir is the direction of the wall + you want to put on. + + Please check the sample implementation in agents/random_agent.py or agents/human_agent.py for more details. + """ + # dummy return + return my_pos, self.dir_map["u"] diff --git a/agents/test_agent.py b/agents/test_agent.py index cf10945..711a7de 100644 --- a/agents/test_agent.py +++ b/agents/test_agent.py @@ -1,9 +1,9 @@ + # Student agent: Add your own agent here from agents.agent import Agent from store import register_agent import sys - @register_agent("test_agent") class TestAgent(Agent): @@ -61,7 +61,9 @@ def get_valid_moves(self, chess_board, my_pos, max_step): print(chess_board.shape) - + + + diff --git a/simulator.py b/simulator.py index b6a1af9..838d4a5 100644 --- a/simulator.py +++ b/simulator.py @@ -83,6 +83,7 @@ def reset(self, swap_players=False, board_size=None): def run(self, swap_players=False, board_size=None): self.reset(swap_players=swap_players, board_size=board_size) is_end, p0_score, p1_score = self.world.step() + num_steps = 0 while not is_end: is_end, p0_score, p1_score = self.world.step() logger.info( From cd53512c44213a476c497f9f0fb629b3724669e1 Mon Sep 17 00:00:00 2001 From: FFFlora0349 <59624826+FFFlora0349@users.noreply.github.com> Date: Mon, 4 Apr 2022 23:00:58 -0400 Subject: [PATCH 07/25] minimax() written, but unset_barrier()? --- agents/.DS_Store | Bin 0 -> 6148 bytes agents/search_tree.py | 9 --- agents/student_agent.py | 155 ++++++++++++++++++++++++++++++++++++---- agents/test_agent.py | 4 +- simulator.py | 1 - 5 files changed, 145 insertions(+), 24 deletions(-) create mode 100644 agents/.DS_Store delete mode 100644 agents/search_tree.py diff --git a/agents/.DS_Store b/agents/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..a292f56e49e07b1b584c4550f1aadb55b0d12f63 GIT binary patch literal 6148 zcmeHKJ8r`;3?&;60kUMws4L_KLXe&y2XONPG8k}yqC+=NIaiL>$IoK8-JB(O1gIxb zd=m5q(-aZieP6aBYY|z(4drgb)NJ2;Vs9ByARK3$tC`N@F;svGyp96)eJF6lnm7ddrvrnx0Kf)e zH_W}402T`XYvK@y2uy*H-`E;s5miV-i best_score: best_score = score best_move = my_new_pos @@ -95,20 +119,127 @@ def step(self, chess_board, my_pos, adv_pos, max_step): return best_move, best_dir_wall - - def minimax(chess_board, my_new_pos, adv_pos, is_maximizing, depth): + def minimax(self, chess_board, my_pos, adv_pos, max_step, is_maximizing, depth): """ is_maximizing is a bool indicating whether we are at maximizing or minimizing step depth is the depth of search """ + # Check base cases: + is_end, s1, s2 = self.world.check_endgame() + if is_end & (not self.world.turn): # if we win. Is it correct to use self.world.turn? + return 1 + elif is_end & self.world.turn: # if the other agent wins + return 0 + elif is_end & s1==s2: # if a draw + return 0.5 + + moves = ((-1, 0), (0, 1), (1, 0), (0, -1)) + best_score = 0 + + my_new_pos = (-1,-1) + dir_wall = -1 + + # Stores all previous moves that we have searched + previous_moves = [(-1,-1), -1] + + if is_maximizing: + + for step_size in range(1, max_step+1): # eg. when moving 4 steps + for _ in range (0, 2): # we make this number of different moves for our agent in this step_size + # eg. we take 2 different 4-step moves of the agent as a possibility to search + for each_step in range (1, step_size+1): # move to each square... + + # decide a direction to move in + dir_move = np.random.randint(0, 4) + + # we place a wall only on the last step: + if each_step == step_size: + dir_wall = np.random.randint(0,4) + + while not self.world.check_valid_step(my_pos, my_pos + moves[dir_move], dir_wall): + + # generate a valid next step: + dir_move = np.random.randint(0,4) + + # we place a wall only on the last step: + if each_step == step_size: + dir_wall = np.random.randint(0,4) + + # If we have already checked this move, do new move... (back to while) + if (dir_move, dir_wall) in previous_moves: + dir_move = (-1,-1) + dir_wall = -1 + + my_new_pos = my_pos + moves[dir_move] + previous_moves = previous_moves + [(my_new_pos, dir_wall)] + if each_step == step_size: + r, c = my_new_pos + self.set_barrier(chess_board, r, c, dir_wall) # ! and to unset_barrier at the same position later? + + # a new move of step-size steps has been generated here--- + + # Run the minimax algo to check where to place our agent is the best + score = self.minimax(chess_board, my_new_pos, adv_pos, max_step, False, depth+1) + + # Get the optimal ? position and wall direction + r, c = my_new_pos + self.unset_barrier(chess_board, r, c, dir_wall) + if score > best_score: + best_score = score + return best_score - return false + else: # if not is_maximizing: + + for step_size in range(1, max_step+1): # eg. when moving 4 steps + for _ in range (0, 2): # we make this number of different moves for our agent in this step_size + # eg. we take 2 different 4-step moves of the agent as a possibility to search + for each_step in range (1, step_size+1): # move to each square... + + # decide a direction to move in + dir_move = np.random.randint(0, 4) + + # we place a wall only on the last step: + if each_step == step_size: + dir_wall = np.random.randint(0,4) + + while not self.world.check_valid_step(my_pos, my_pos + moves[dir_move], dir_wall): + + # generate a valid next step: + dir_move = np.random.randint(0,4) + + # we place a wall only on the last step: + if each_step == step_size: + dir_wall = np.random.randint(0,4) + + # If we have already checked this move, do new move... (back to while) + if (dir_move, dir_wall) in previous_moves: + dir_move = (-1,-1) + dir_wall = -1 + + my_new_pos = my_pos + moves[dir_move] + previous_moves = previous_moves + [(my_new_pos, dir_wall)] + if each_step == step_size: + r, c = my_new_pos + self.set_barrier(chess_board, r, c, dir_wall) # ! and to unset_barrier at the same position later? + + # a new move of step-size steps has been generated here--- + + # Run the minimax algo to check where to place our agent is the best + score = self.minimax(chess_board, my_new_pos, adv_pos, max_step, True, depth+1) # the TRUE here + + # Get the optimal ? position and wall direction + r, c = my_new_pos + self.unset_barrier(chess_board, r, c, dir_wall) + + if score < best_score: # the LESS THAN here + best_score = score + return best_score + # M x M board. max_step = floor((M+1)/2) # for each node in the search tree, it has children for every possible step number : # some children after moving 1 step, moving 2 steps, ... 5 steps max. # to reduce the branching factor of the search tree but still cover each possible distance, # we can calculate only 2 positions for each number <= max_step, and 4 positions for the wall. - - + \ No newline at end of file diff --git a/agents/test_agent.py b/agents/test_agent.py index 711a7de..f977635 100644 --- a/agents/test_agent.py +++ b/agents/test_agent.py @@ -52,7 +52,7 @@ def step(self, chess_board, my_pos, adv_pos, max_step): def check_valid_input(self, x, y, dir, x_max, y_max): return 0 <= x < x_max and 0 <= y < y_max and dir in self.dir_map - def get_valid_moves(self, chess_board, my_pos, max_step): + '''def get_valid_moves(self, chess_board, my_pos, max_step): print(my_pos) board_size = chess_board.shape[0] num_steps = max_step @@ -61,7 +61,7 @@ def get_valid_moves(self, chess_board, my_pos, max_step): print(chess_board.shape) - + ''' diff --git a/simulator.py b/simulator.py index 838d4a5..b6a1af9 100644 --- a/simulator.py +++ b/simulator.py @@ -83,7 +83,6 @@ def reset(self, swap_players=False, board_size=None): def run(self, swap_players=False, board_size=None): self.reset(swap_players=swap_players, board_size=board_size) is_end, p0_score, p1_score = self.world.step() - num_steps = 0 while not is_end: is_end, p0_score, p1_score = self.world.step() logger.info( From 8fdc223cb72af882d45ed188d501c2307e52b316 Mon Sep 17 00:00:00 2001 From: cWetaski Date: Wed, 6 Apr 2022 18:40:19 -0400 Subject: [PATCH 08/25] Completed function in test_agent.py which outputs all valid moves for an agent --- agents/test_agent.py | 92 +++++++++++++++++++++++++++++++++++++--- gameplayer.py => play.py | 1 + 2 files changed, 86 insertions(+), 7 deletions(-) rename gameplayer.py => play.py (88%) diff --git a/agents/test_agent.py b/agents/test_agent.py index f977635..402d609 100644 --- a/agents/test_agent.py +++ b/agents/test_agent.py @@ -3,6 +3,7 @@ from agents.agent import Agent from store import register_agent import sys +import numpy as np @register_agent("test_agent") class TestAgent(Agent): @@ -18,7 +19,7 @@ def __init__(self): } def step(self, chess_board, my_pos, adv_pos, max_step): - self.get_valid_moves(chess_board, my_pos, max_step) + self.get_valid_moves(chess_board, my_pos, adv_pos, max_step) text = input("Your move (x,y,dir) or input q to quit: ") while len(text.split(",")) != 3 and "q" not in text.lower(): @@ -52,16 +53,93 @@ def step(self, chess_board, my_pos, adv_pos, max_step): def check_valid_input(self, x, y, dir, x_max, y_max): return 0 <= x < x_max and 0 <= y < y_max and dir in self.dir_map - '''def get_valid_moves(self, chess_board, my_pos, max_step): + def get_valid_moves(self, chess_board, my_pos, adv_pos, max_step): # returns set (can change to list if necessary) of valid moves from current position print(my_pos) + print(max_step) board_size = chess_board.shape[0] - num_steps = max_step - while (num_steps > 0) - + print('board size is %d' % (board_size)) + end_posits = [] + for n in range(1,max_step+1): + for r_dist in range(0,n+1): + c_dist = n - r_dist + print('%d %d %d' % (n,r_dist,c_dist)) + if r_dist == 0: + cur_moves = [(my_pos[0],my_pos[1] + c_dist),(my_pos[0],my_pos[1] - c_dist)] + elif c_dist == 0: + cur_moves = [(my_pos[0] + r_dist,my_pos[1]),(my_pos[0] - r_dist,my_pos[1])] + else: + cur_moves = [(my_pos[0] + r_dist,my_pos[1] + c_dist),( + my_pos[0] + r_dist,my_pos[1] - c_dist),( + my_pos[0] - r_dist,my_pos[1] + c_dist),( + my_pos[0] - r_dist,my_pos[1] - c_dist)] + print(cur_moves) + end_posits.extend(cur_moves) + + print(end_posits) + end_posits = set(filter(lambda end_pos: end_pos[0] < board_size and end_pos[1] < board_size and end_pos[0] >= 0 and end_pos[1] >= 0 and end_pos != adv_pos, end_posits)) # filter moves which leave boundary or end in adversary's location + print('filtered') + print(end_posits) + moves = [] + for end_pos in end_posits: + moves.append(tuple((end_pos[0],end_pos[1],0))) + moves.append(tuple((end_pos[0],end_pos[1],1))) + moves.append(tuple((end_pos[0],end_pos[1],2))) + moves.append(tuple((end_pos[0],end_pos[1],3))) + + moves = set(filter(lambda move: self.check_valid_step(np.asarray(my_pos),[move[0],move[1]],adv_pos, move[2], chess_board, max_step),moves)) + print(moves) + return moves + + def check_valid_step(self, start_pos, end_pos, adv_pos, barrier_dir, chess_board, max_step): # reused from world.py (modified to work in this context) + """ + Check if the step the agent takes is valid (reachable and within max steps). + + Parameters + ---------- + start_pos : tuple + The start position of the agent. + end_pos : np.ndarray + The end position of the agent. + barrier_dir : int + The direction of the barrier. + """ + print(type(adv_pos)) + print(type(start_pos)) + moves = ((-1, 0), (0, 1), (1, 0), (0, -1)) + + # Endpoint already has barrier or is boarder + r, c = end_pos + if chess_board[r, c, barrier_dir]: + return False + if np.array_equal(start_pos, end_pos): + return True + + # BFS + state_queue = [(start_pos, 0)] + visited = {tuple(start_pos)} + is_reached = False + while state_queue and not is_reached: + cur_pos, cur_step = state_queue.pop(0) + print(cur_pos) + r, c = cur_pos + if cur_step == max_step: + break + for dir, move in enumerate(moves): + if chess_board[r, c, dir]: + continue + + next_pos = cur_pos + move + if np.array_equal(next_pos, adv_pos) or tuple(next_pos) in visited: + continue + if np.array_equal(next_pos, end_pos): + is_reached = True + break + + visited.add(tuple(next_pos)) + state_queue.append((next_pos, cur_step + 1)) - print(chess_board.shape) - ''' + return is_reached diff --git a/gameplayer.py b/play.py similarity index 88% rename from gameplayer.py rename to play.py index fee2d11..41d7259 100644 --- a/gameplayer.py +++ b/play.py @@ -3,6 +3,7 @@ args = simulator.get_args() args.player_1 = "test_agent" args.player_2 = "random_agent" +args.display = True s1 = simulator.Simulator(args) result = s1.run() From 77065938e5a1e9d68f86545ee8694035be808e44 Mon Sep 17 00:00:00 2001 From: cWetaski Date: Wed, 6 Apr 2022 22:07:13 -0400 Subject: [PATCH 09/25] Added functions to test_agent.py to determine if a proposed step will end the game and (if the game is ended) who wins --- agents/test_agent.py | 152 +++++++++++++++++++++++++++++++++---------- 1 file changed, 116 insertions(+), 36 deletions(-) diff --git a/agents/test_agent.py b/agents/test_agent.py index 402d609..eb0ea07 100644 --- a/agents/test_agent.py +++ b/agents/test_agent.py @@ -4,6 +4,7 @@ from store import register_agent import sys import numpy as np +from copy import deepcopy @register_agent("test_agent") class TestAgent(Agent): @@ -17,9 +18,20 @@ def __init__(self): "d": 2, "l": 3, } + self.moves = ((-1, 0), (0, 1), (1, 0), (0, -1)) # moves as defined in world.py (useful for reusing world.py code) + self.opposites = {0: 2, 1: 3, 2: 0, 3: 1} # opposite moves as defined in world.py def step(self, chess_board, my_pos, adv_pos, max_step): - self.get_valid_moves(chess_board, my_pos, adv_pos, max_step) + valid_steps = self.get_valid_steps(chess_board, my_pos, adv_pos, max_step) + terminal_steps = set() + for step in valid_steps: + is_endgame, is_winner = self.sim_move(chess_board,step,adv_pos)[1:3] + if(is_endgame == True): + terminal_steps.add(tuple((step,is_winner))) + print("Valid Steps:") + print(valid_steps) + print("Terminal Steps:") + print(terminal_steps) text = input("Your move (x,y,dir) or input q to quit: ") while len(text.split(",")) != 3 and "q" not in text.lower(): @@ -53,43 +65,32 @@ def step(self, chess_board, my_pos, adv_pos, max_step): def check_valid_input(self, x, y, dir, x_max, y_max): return 0 <= x < x_max and 0 <= y < y_max and dir in self.dir_map - def get_valid_moves(self, chess_board, my_pos, adv_pos, max_step): # returns set (can change to list if necessary) of valid moves from current position - print(my_pos) - print(max_step) + def get_valid_steps(self, chess_board, my_pos, adv_pos, max_step): # returns set (can change to list if necessary) of valid steps from current position board_size = chess_board.shape[0] - print('board size is %d' % (board_size)) end_posits = [] + valid_steps = [] for n in range(1,max_step+1): for r_dist in range(0,n+1): - c_dist = n - r_dist - print('%d %d %d' % (n,r_dist,c_dist)) if r_dist == 0: - cur_moves = [(my_pos[0],my_pos[1] + c_dist),(my_pos[0],my_pos[1] - c_dist)] + cur_steps = [(my_pos[0],my_pos[1] + c_dist),(my_pos[0],my_pos[1] - c_dist)] elif c_dist == 0: - cur_moves = [(my_pos[0] + r_dist,my_pos[1]),(my_pos[0] - r_dist,my_pos[1])] + cur_steps = [(my_pos[0] + r_dist,my_pos[1]),(my_pos[0] - r_dist,my_pos[1])] else: - cur_moves = [(my_pos[0] + r_dist,my_pos[1] + c_dist),( + cur_steps = [(my_pos[0] + r_dist,my_pos[1] + c_dist),( my_pos[0] + r_dist,my_pos[1] - c_dist),( my_pos[0] - r_dist,my_pos[1] + c_dist),( my_pos[0] - r_dist,my_pos[1] - c_dist)] - print(cur_moves) - end_posits.extend(cur_moves) - - print(end_posits) - end_posits = set(filter(lambda end_pos: end_pos[0] < board_size and end_pos[1] < board_size and end_pos[0] >= 0 and end_pos[1] >= 0 and end_pos != adv_pos, end_posits)) # filter moves which leave boundary or end in adversary's location - print('filtered') - print(end_posits) - moves = [] + end_posits.extend(cur_steps) + # filter steps which leave boundary or end in adversary's location + end_posits = set(filter(lambda end_pos: end_pos[0] < board_size and end_pos[1] < board_size and end_pos[0] >= 0 and end_pos[1] >= 0 and end_pos != adv_pos, end_posits)) for end_pos in end_posits: - moves.append(tuple((end_pos[0],end_pos[1],0))) - moves.append(tuple((end_pos[0],end_pos[1],1))) - moves.append(tuple((end_pos[0],end_pos[1],2))) - moves.append(tuple((end_pos[0],end_pos[1],3))) - - moves = set(filter(lambda move: self.check_valid_step(np.asarray(my_pos),[move[0],move[1]],adv_pos, move[2], chess_board, max_step),moves)) - print(moves) - return moves + valid_steps.append(tuple((end_pos[0],end_pos[1],0))) + valid_steps.append(tuple((end_pos[0],end_pos[1],1))) + valid_steps.append(tuple((end_pos[0],end_pos[1],2))) + valid_steps.append(tuple((end_pos[0],end_pos[1],3))) + valid_steps = set(filter(lambda move: self.check_valid_step(np.asarray(my_pos),[move[0],move[1]],adv_pos, move[2], chess_board, max_step),valid_steps)) + return valid_steps def check_valid_step(self, start_pos, end_pos, adv_pos, barrier_dir, chess_board, max_step): # reused from world.py (modified to work in this context) """ @@ -104,43 +105,122 @@ def check_valid_step(self, start_pos, end_pos, adv_pos, barrier_dir, chess_board barrier_dir : int The direction of the barrier. """ - print(type(adv_pos)) - print(type(start_pos)) - moves = ((-1, 0), (0, 1), (1, 0), (0, -1)) - # Endpoint already has barrier or is boarder r, c = end_pos if chess_board[r, c, barrier_dir]: return False if np.array_equal(start_pos, end_pos): return True - # BFS state_queue = [(start_pos, 0)] visited = {tuple(start_pos)} is_reached = False while state_queue and not is_reached: cur_pos, cur_step = state_queue.pop(0) - print(cur_pos) r, c = cur_pos if cur_step == max_step: break - for dir, move in enumerate(moves): + for dir, move in enumerate(self.moves): if chess_board[r, c, dir]: continue - next_pos = cur_pos + move if np.array_equal(next_pos, adv_pos) or tuple(next_pos) in visited: continue if np.array_equal(next_pos, end_pos): is_reached = True break - visited.add(tuple(next_pos)) state_queue.append((next_pos, cur_step + 1)) - return is_reached + def sim_move(self,chess_board,move,adv_pos): + """ + Assumption is that move the move is valid + Will add the move to a copy of the chess_board, and check if the move ends the game + + Returns + ------- + chess_board_copy : chess_board + Copy of chess_board with move applied + is_endgame : bool + Whether the game ends + is_winner : bool + (if is_endgame == true) Whether the player who made the move wins + (if is_endgame == false or if game is a tie) None + """ + # create copy of the chess_board to view the new gamestate + chess_board_copy = deepcopy(chess_board) + + # apply move to copied chess_board + self.set_barrier(chess_board_copy,move[0],move[1],move[2]) + + # check if move ends game + cur_pos = (move[0],move[1]) + is_endgame, is_winner = self.check_endgame(chess_board_copy,cur_pos,adv_pos) + return chess_board_copy, is_endgame, is_winner + + def set_barrier(self, chess_board, r, c, dir): # adapted from world.py function of same name + # Set the barrier to True + chess_board[r, c, dir] = True + # Set the opposite barrier to True + move = self.moves[dir] + chess_board[r + move[0], c + move[1], self.opposites[dir]] = True + + def check_endgame(self,chess_board,p0_pos,p1_pos): # adapted from world.py function of same name + """ + Check if a game state is terminal and return who wins if so + Does not return score since it is not useful + + Returns + ------- + is_endgame : bool + Whether the game ends. + is_p0_winner : bool + Whether p0 wins (if is_endgame == false or if there is a tie, returns None) + """ + # Union-Find + father = dict() + for r in range(chess_board.shape[0]): + for c in range(chess_board.shape[0]): + father[(r, c)] = (r, c) + + def find(pos): + if father[pos] != pos: + father[pos] = find(father[pos]) + return father[pos] + + def union(pos1, pos2): + father[pos1] = pos2 + + for r in range(chess_board.shape[0]): + for c in range(chess_board.shape[0]): + for dir, move in enumerate( + self.moves[1:3] + ): # Only check down and right + if chess_board[r, c, dir + 1]: + continue + pos_a = find((r, c)) + pos_b = find((r + move[0], c + move[1])) + if pos_a != pos_b: + union(pos_a, pos_b) + + for r in range(chess_board.shape[0]): + for c in range(chess_board.shape[0]): + find((r, c)) + p0_r = find(tuple(p0_pos)) + p1_r = find(tuple(p1_pos)) + p0_score = list(father.values()).count(p0_r) + p1_score = list(father.values()).count(p1_r) + if p0_r == p1_r: + return False, None + is_p0_winner = None + if p0_score > p1_score: + is_p0_winner = True + elif p0_score < p1_score: + is_p0_winner = False + return True, is_p0_winner + + From 210c4ba5af4b09b03a63efdf74a53fd4ef2371e7 Mon Sep 17 00:00:00 2001 From: FFFlora0349 <59624826+FFFlora0349@users.noreply.github.com> Date: Thu, 7 Apr 2022 00:27:50 -0400 Subject: [PATCH 10/25] Modified set_barrier(), check_valid_step(), check_endgame() --- .vscode/launch.json | 15 +++ .vscode/settings.json | 3 + Strategizing_Notes.txt | 4 +- agents/.DS_Store | Bin 6148 -> 6148 bytes agents/student_agent.py | 226 +++++++++++++++++++++++++++++++++------- world.py | 3 +- 6 files changed, 214 insertions(+), 37 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..7a9dfa0 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "pwa-chrome", + "request": "launch", + "name": "Launch Chrome against localhost", + "url": "http://localhost:8080", + "webRoot": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..005b40d --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "workbench.preferredDarkColorTheme": "Visual Studio Light" +} \ No newline at end of file diff --git a/Strategizing_Notes.txt b/Strategizing_Notes.txt index 70e8b91..15f4406 100644 --- a/Strategizing_Notes.txt +++ b/Strategizing_Notes.txt @@ -17,4 +17,6 @@ UPDATE: We want to reduce the branching factor of the search tree but still cover each possible distance. eg. for a max_step equal to 5, we search some move of distance 1, some move of distance 2, some of distance 3, 4, and 5. -To implement the minimax algo... +Implemented the minimax algo. + +How to call the functions like check_valid_step() from world.py in student_agent.py file?? \ No newline at end of file diff --git a/agents/.DS_Store b/agents/.DS_Store index a292f56e49e07b1b584c4550f1aadb55b0d12f63..38734ca2de71d90578b12a191d5ff30a57f26d5c 100644 GIT binary patch delta 14 VcmZoMXfc?uZsW!<_Qfn50st)X1tkCg delta 17 YcmZoMXfc?uj*)TW#xVBHtQ`OO0Xe${HUIzs diff --git a/agents/student_agent.py b/agents/student_agent.py index a5b23c4..f3b7938 100644 --- a/agents/student_agent.py +++ b/agents/student_agent.py @@ -24,13 +24,16 @@ def __init__(self): "d": 2, "l": 3, } + self.autoplay = True + # functions set_barrier(), check_valid_step(), check_endgame() below copied from world.py: def set_barrier(self, chess_board, r, c, dir): # Set the barrier to True chess_board[r, c, dir] = True moves = ((-1, 0), (0, 1), (1, 0), (0, -1)) opposites = {0: 2, 1: 3, 2: 0, 3: 1} + # Set the opposite barrier to True move = moves[dir] chess_board[r + move[0], c + move[1], opposites[dir]] = True @@ -41,11 +44,134 @@ def unset_barrier(self, chess_board, r, c, dir): moves = ((-1, 0), (0, 1), (1, 0), (0, -1)) opposites = {0: 2, 1: 3, 2: 0, 3: 1} + # Set the opposite barrier to False move = moves[dir] chess_board[r + move[0], c + move[1], opposites[dir]] = False + def check_valid_step(self, chess_board, my_start_pos, my_end_pos, barrier_dir, adv_pos, max_step): + """ + Check if the step the agent takes is valid (reachable and within max steps). + + Parameters + ---------- + start_pos : tuple + The start position of the agent. + end_pos : np.ndarray + The end position of the agent. + barrier_dir : int + The direction of the barrier. + """ + moves = ((-1, 0), (0, 1), (1, 0), (0, -1)) + + # Endpoint already has barrier or is boarder + print(my_end_pos) + r, c = my_end_pos + if chess_board[r, c, barrier_dir]: + return False + if np.array_equal(my_start_pos, my_end_pos): + return True + + # Get position of the adversary... deleted + + # BFS + state_queue = [(my_start_pos, 0)] + visited = {tuple(my_start_pos)} + is_reached = False + while state_queue and not is_reached: + cur_pos, cur_step = state_queue.pop(0) + r, c = cur_pos + if cur_step == max_step: + break + for dir, move in enumerate(moves): + if chess_board[r, c, dir]: + continue + + next_pos = cur_pos + move + if np.array_equal(next_pos, adv_pos) or tuple(next_pos) in visited: + continue + if np.array_equal(next_pos, my_end_pos): + is_reached = True + break + + visited.add(tuple(next_pos)) + state_queue.append((next_pos, cur_step + 1)) + + return is_reached + + def check_endgame(self, chess_board, my_pos, adv_pos): + """ + Check if the game ends and compute the current score of the agents. + + Returns + ------- + is_endgame : bool + Whether the game ends. + player_1_score : int + The score of player 1. + player_2_score : int + The score of player 2. + """ + moves = ((-1, 0), (0, 1), (1, 0), (0, -1)) + board_size = chess_board.shape[1] # array3d.shape ---> (layer,row,column) + + # Union-Find + father = dict() + for r in range(board_size): + for c in range(board_size): + father[(r, c)] = (r, c) + + def find(pos): + if father[pos] != pos: + father[pos] = find(father[pos]) + return father[pos] + + def union(pos1, pos2): + father[pos1] = pos2 + + for r in range(board_size): + for c in range(board_size): + for dir, move in enumerate( + moves[1:3] + ): # Only check down and right + if chess_board[r, c, dir + 1]: + continue + pos_a = find((r, c)) + pos_b = find((r + move[0], c + move[1])) + if pos_a != pos_b: + union(pos_a, pos_b) + + for r in range(board_size): + for c in range(board_size): + find((r, c)) + # + p0_r = find(tuple(my_pos)) + p1_r = find(tuple(adv_pos)) + p0_score = list(father.values()).count(p0_r) + p1_score = list(father.values()).count(p1_r) + if p0_r == p1_r: + return False, p0_score, p1_score + ''' + player_win = None + win_blocks = -1 + if p0_score > p1_score: + player_win = 0 + win_blocks = p0_score + elif p0_score < p1_score: + player_win = 1 + win_blocks = p1_score + else: + player_win = -1 # Tie + if player_win >= 0: + logging.info( + f"Game ends! Player {self.player_names[player_win]} wins having control over {win_blocks} blocks!" + ) + else: + logging.info("Game ends! It is a Tie!") + ''' + return True, p0_score, p1_score + # THE ACTAUL IMPLEMENTATION... def step(self, chess_board, my_pos, adv_pos, max_step): """ Implement the step function of your agent here. @@ -64,16 +190,16 @@ def step(self, chess_board, my_pos, adv_pos, max_step): moves = ((-1, 0), (0, 1), (1, 0), (0, -1)) best_score = 0 - best_move = [(-1,-1), -1] + best_move = (-1,-1) my_new_pos = (-1,-1) dir_wall = -1 # Stores all previous moves that we have searched - previous_moves = [(-1,-1), -1] + previous_moves = [((-1,-1), -1)] for step_size in range(1, max_step+1): # eg. when moving 4 steps - for _ in range (0, 2): # we make this number of different moves for our agent in this step_size + for _ in range (0, 3): # we make this number of different moves for our agent in this step_size # eg. we take 2 different 4-step moves of the agent as a possibility to search for each_step in range (1, step_size+1): # move to each square... @@ -84,8 +210,13 @@ def step(self, chess_board, my_pos, adv_pos, max_step): if each_step == step_size: dir_wall = np.random.randint(0,4) - while not self.world.check_valid_step(my_pos, my_pos + moves[dir_move], dir_wall): - + # Go to a new position + r,c = my_pos + rd,cd = moves[dir_move] + my_new_pos = (r+rd, c+cd) + + while not self.check_valid_step(chess_board, my_pos, my_new_pos, dir_wall, adv_pos, max_step): + # generate a valid next step: dir_move = np.random.randint(0,4) @@ -97,16 +228,21 @@ def step(self, chess_board, my_pos, adv_pos, max_step): if (dir_move, dir_wall) in previous_moves: dir_move = (-1,-1) dir_wall = -1 - - my_new_pos = my_pos + moves[dir_move] - previous_moves = previous_moves + [(my_new_pos, dir_wall)] + r,c = my_pos + rd,cd = moves[dir_move] + my_new_pos = (r+rd, c+cd) + + # If we are at the last step if each_step == step_size: r, c = my_new_pos - self.set_barrier(chess_board, r, c, dir_wall) # ! and to unset_barrier at the same position later? - + self.set_barrier(chess_board, r, c, dir_wall) # and unset_barrier at the same position later + # Add this move to the move history + previous_moves = previous_moves + [(my_new_pos, dir_wall)] + # a new move of step-size steps has been generated here--- # Run the minimax algo to check where to place our agent is the best + # this is the minimizing node score = self.minimax(chess_board, my_new_pos, adv_pos, max_step, False, 10) # Get the optimal ? position and wall direction @@ -125,13 +261,15 @@ def minimax(self, chess_board, my_pos, adv_pos, max_step, is_maximizing, depth): depth is the depth of search """ # Check base cases: - is_end, s1, s2 = self.world.check_endgame() - if is_end & (not self.world.turn): # if we win. Is it correct to use self.world.turn? - return 1 - elif is_end & self.world.turn: # if the other agent wins - return 0 - elif is_end & s1==s2: # if a draw + is_end, s1, s2 = self.check_endgame(chess_board, my_pos, adv_pos) + # if a draw: + if is_end & s1==s2: return 0.5 + # if not draw: + # if the max player wins + if is_end & (not is_maximizing): return 1 + # if the min player wins + elif is_end & is_maximizing: return 0 moves = ((-1, 0), (0, 1), (1, 0), (0, -1)) best_score = 0 @@ -139,13 +277,13 @@ def minimax(self, chess_board, my_pos, adv_pos, max_step, is_maximizing, depth): my_new_pos = (-1,-1) dir_wall = -1 - # Stores all previous moves that we have searched - previous_moves = [(-1,-1), -1] + # Stores all previous moves that we searched in each for loop + previous_moves = [((-1,-1), -1)] if is_maximizing: for step_size in range(1, max_step+1): # eg. when moving 4 steps - for _ in range (0, 2): # we make this number of different moves for our agent in this step_size + for _ in range (0, 3): # we make this number of different moves for our agent in this step_size # eg. we take 2 different 4-step moves of the agent as a possibility to search for each_step in range (1, step_size+1): # move to each square... @@ -156,7 +294,11 @@ def minimax(self, chess_board, my_pos, adv_pos, max_step, is_maximizing, depth): if each_step == step_size: dir_wall = np.random.randint(0,4) - while not self.world.check_valid_step(my_pos, my_pos + moves[dir_move], dir_wall): + # Go to a new position + r,c = my_pos + rd,cd = moves[dir_move] + my_new_pos = (r+rd, c+cd) + while not self.check_valid_step(chess_board, my_pos, my_new_pos, dir_wall, adv_pos, max_step): # generate a valid next step: dir_move = np.random.randint(0,4) @@ -169,13 +311,17 @@ def minimax(self, chess_board, my_pos, adv_pos, max_step, is_maximizing, depth): if (dir_move, dir_wall) in previous_moves: dir_move = (-1,-1) dir_wall = -1 + r,c = my_pos + rd,cd = moves[dir_move] + my_new_pos = (r+rd, c+cd) - my_new_pos = my_pos + moves[dir_move] - previous_moves = previous_moves + [(my_new_pos, dir_wall)] + # If we are at the last step if each_step == step_size: r, c = my_new_pos - self.set_barrier(chess_board, r, c, dir_wall) # ! and to unset_barrier at the same position later? - + self.set_barrier(chess_board, r, c, dir_wall) # and unset_barrier at the same position later + # Add this move to the move history + previous_moves = previous_moves + [(my_new_pos, dir_wall)] + # a new move of step-size steps has been generated here--- # Run the minimax algo to check where to place our agent is the best @@ -188,10 +334,10 @@ def minimax(self, chess_board, my_pos, adv_pos, max_step, is_maximizing, depth): best_score = score return best_score - else: # if not is_maximizing: + else: # if not is_maximizing: for step_size in range(1, max_step+1): # eg. when moving 4 steps - for _ in range (0, 2): # we make this number of different moves for our agent in this step_size + for _ in range (0, 3): # we make this number of different moves for our agent in this step_size # eg. we take 2 different 4-step moves of the agent as a possibility to search for each_step in range (1, step_size+1): # move to each square... @@ -201,8 +347,14 @@ def minimax(self, chess_board, my_pos, adv_pos, max_step, is_maximizing, depth): # we place a wall only on the last step: if each_step == step_size: dir_wall = np.random.randint(0,4) - - while not self.world.check_valid_step(my_pos, my_pos + moves[dir_move], dir_wall): + + # Go to a new position + r,c = adv_pos + rd,cd = moves[dir_move] + adv_new_pos = (r+rd, c+cd) + # Now we are taking a step for the adversary... + # The arguments are pos, new_pos, ..., adv_pos, + while not self.check_valid_step(chess_board, adv_pos, adv_new_pos, dir_wall, my_pos, max_step): # generate a valid next step: dir_move = np.random.randint(0,4) @@ -215,20 +367,24 @@ def minimax(self, chess_board, my_pos, adv_pos, max_step, is_maximizing, depth): if (dir_move, dir_wall) in previous_moves: dir_move = (-1,-1) dir_wall = -1 + r,c = adv_pos + rd,cd = moves[dir_move] + adv_new_pos = (r+rd, c+cd) - my_new_pos = my_pos + moves[dir_move] - previous_moves = previous_moves + [(my_new_pos, dir_wall)] + # If we are at the last step if each_step == step_size: - r, c = my_new_pos - self.set_barrier(chess_board, r, c, dir_wall) # ! and to unset_barrier at the same position later? - + r, c = adv_new_pos + self.set_barrier(chess_board, r, c, dir_wall) # and unset_barrier at the same position later + # Add this move to the move history + previous_moves = previous_moves + [(adv_new_pos, dir_wall)] + # a new move of step-size steps has been generated here--- # Run the minimax algo to check where to place our agent is the best - score = self.minimax(chess_board, my_new_pos, adv_pos, max_step, True, depth+1) # the TRUE here + score = self.minimax(chess_board, my_pos, adv_new_pos, max_step, True, depth+1) # the TRUE here # Get the optimal ? position and wall direction - r, c = my_new_pos + r, c = adv_new_pos self.unset_barrier(chess_board, r, c, dir_wall) if score < best_score: # the LESS THAN here diff --git a/world.py b/world.py index eee7ef3..3578469 100644 --- a/world.py +++ b/world.py @@ -28,6 +28,7 @@ def __init__( autoplay=False, ): """ + Below is the initialization function __init__(): Initialize the game world Parameters @@ -396,7 +397,7 @@ def set_barrier(self, r, c, dir): # Set the opposite barrier to True move = self.moves[dir] self.chess_board[r + move[0], c + move[1], self.opposites[dir]] = True - + def random_walk(self, my_pos, adv_pos): """ Randomly walk to the next position in the board. From f6f10aa6ebbd5a2210729e4cd40e3b220fa058f0 Mon Sep 17 00:00:00 2001 From: cWetaski Date: Thu, 7 Apr 2022 00:44:24 -0400 Subject: [PATCH 11/25] Added some more helper functions incl count_edges, get_empty_edges, and started working on a get_stupid_steps funciton to identify bad moves --- agents/test_agent.py | 79 +++++++++++++++++++++++++++++++++++++++----- play.py | 1 + 2 files changed, 71 insertions(+), 9 deletions(-) diff --git a/agents/test_agent.py b/agents/test_agent.py index eb0ea07..3ca2648 100644 --- a/agents/test_agent.py +++ b/agents/test_agent.py @@ -1,5 +1,6 @@ # Student agent: Add your own agent here +from operator import truediv from agents.agent import Agent from store import register_agent import sys @@ -19,19 +20,28 @@ def __init__(self): "l": 3, } self.moves = ((-1, 0), (0, 1), (1, 0), (0, -1)) # moves as defined in world.py (useful for reusing world.py code) - self.opposites = {0: 2, 1: 3, 2: 0, 3: 1} # opposite moves as defined in world.py + self.opposites = {0: 2, 1: 3, 2: 0, 3: 1} # opposite directions as defined in world.py def step(self, chess_board, my_pos, adv_pos, max_step): valid_steps = self.get_valid_steps(chess_board, my_pos, adv_pos, max_step) terminal_steps = set() + stupid_steps = set() for step in valid_steps: is_endgame, is_winner = self.sim_move(chess_board,step,adv_pos)[1:3] + num_edges = self.count_edges(chess_board,(step[0],step[1])) if(is_endgame == True): terminal_steps.add(tuple((step,is_winner))) + if(num_edges == 3): + stupid_steps.add(step) print("Valid Steps:") print(valid_steps) print("Terminal Steps:") print(terminal_steps) + print("Stupid Steps:") + print(stupid_steps) + not_stupid_steps = self.filter_stupid_steps(chess_board,valid_steps,adv_pos,max_step) + print(not_stupid_steps) + text = input("Your move (x,y,dir) or input q to quit: ") while len(text.split(",")) != 3 and "q" not in text.lower(): @@ -67,7 +77,7 @@ def check_valid_input(self, x, y, dir, x_max, y_max): def get_valid_steps(self, chess_board, my_pos, adv_pos, max_step): # returns set (can change to list if necessary) of valid steps from current position board_size = chess_board.shape[0] - end_posits = [] + end_posits = [my_pos] valid_steps = [] for n in range(1,max_step+1): for r_dist in range(0,n+1): @@ -85,10 +95,10 @@ def get_valid_steps(self, chess_board, my_pos, adv_pos, max_step): # returns set # filter steps which leave boundary or end in adversary's location end_posits = set(filter(lambda end_pos: end_pos[0] < board_size and end_pos[1] < board_size and end_pos[0] >= 0 and end_pos[1] >= 0 and end_pos != adv_pos, end_posits)) for end_pos in end_posits: - valid_steps.append(tuple((end_pos[0],end_pos[1],0))) - valid_steps.append(tuple((end_pos[0],end_pos[1],1))) - valid_steps.append(tuple((end_pos[0],end_pos[1],2))) - valid_steps.append(tuple((end_pos[0],end_pos[1],3))) + for dir in range(0,4): + if chess_board[end_pos[0],end_pos[1],dir]: + continue + valid_steps.append(tuple((end_pos[0],end_pos[1],dir))) valid_steps = set(filter(lambda move: self.check_valid_step(np.asarray(my_pos),[move[0],move[1]],adv_pos, move[2], chess_board, max_step),valid_steps)) return valid_steps @@ -111,6 +121,7 @@ def check_valid_step(self, start_pos, end_pos, adv_pos, barrier_dir, chess_board return False if np.array_equal(start_pos, end_pos): return True + # BFS state_queue = [(start_pos, 0)] visited = {tuple(start_pos)} @@ -123,7 +134,7 @@ def check_valid_step(self, start_pos, end_pos, adv_pos, barrier_dir, chess_board for dir, move in enumerate(self.moves): if chess_board[r, c, dir]: continue - next_pos = cur_pos + move + next_pos = (cur_pos[0] + move[0],cur_pos[1]+move[1]) if np.array_equal(next_pos, adv_pos) or tuple(next_pos) in visited: continue if np.array_equal(next_pos, end_pos): @@ -145,15 +156,20 @@ def sim_move(self,chess_board,move,adv_pos): is_endgame : bool Whether the game ends is_winner : bool - (if is_endgame == true) Whether the player who made the move wins - (if is_endgame == false or if game is a tie) None + (if is_endgame == True) Whether the player who made the move wins + (if is_endgame == False or if game is a tie) None """ + # create copy of the chess_board to view the new gamestate chess_board_copy = deepcopy(chess_board) # apply move to copied chess_board self.set_barrier(chess_board_copy,move[0],move[1],move[2]) + # check stupid end + if self.count_edges(chess_board_copy,(move[0],move[1])) == 4: + return chess_board_copy, True, False + # check if move ends game cur_pos = (move[0],move[1]) is_endgame, is_winner = self.check_endgame(chess_board_copy,cur_pos,adv_pos) @@ -220,6 +236,51 @@ def union(pos1, pos2): is_p0_winner = False return True, is_p0_winner + def count_edges(self,chess_board,pos): + count = 0 + for dir in range(0,4): + if chess_board[pos[0],pos[1],dir]: + count = count + 1 + return count + + def get_empty_edges(self,chess_board,pos): + empty_edges = [] + for dir in range(0,4): + if not (chess_board[pos[0],pos[1],dir]): + empty_edges.append(dir) + return empty_edges + + + + def get_stupid_steps(self,chess_board,steps,adv_pos,max_step): + """ + Returns stupid steps + In implementation it will make more sense for it to return steps which aren't stupid, + but for now this makes it easier to see if it is correctly identifying "stupid" steps + """ + filtered_steps = deepcopy(steps) + chess_board_copy = deepcopy(chess_board) + + # stupid condition 1: instantly makes us lose + # No need to query this, since it will already be queried when checking for terminal steps + + # stupid condition 2: put 3 walls around us when adversary is in range + def check_stupid_2(chess_board,step,adv_pos,max_step): + r,c,dir = step + empty_edges = self.get_empty_edges(chess_board,(r,c)) + if (self.count_edges(chess_board_copy,(r,c)) == 2): # might be stupid + empty_edges.remove(dir) + empty_dir = empty_edges[0] + move = self.moves[empty_dir] + win_pos = (r+move[0],c+move[1]) + if self.check_valid_step(tuple(adv_pos),win_pos,(r,c),self.opposites[empty_dir],chess_board,max_step): + return True + return False + + filtered_steps = set(filter(lambda step: check_stupid_2(chess_board,step,adv_pos,max_step),steps)) + return filtered_steps + + diff --git a/play.py b/play.py index 41d7259..7fd3a81 100644 --- a/play.py +++ b/play.py @@ -4,6 +4,7 @@ args.player_1 = "test_agent" args.player_2 = "random_agent" args.display = True +args.board_size = 5 s1 = simulator.Simulator(args) result = s1.run() From f69a3b7b95eb5796f4a0a1733d3327202307773e Mon Sep 17 00:00:00 2001 From: cWetaski Date: Thu, 7 Apr 2022 12:29:10 -0400 Subject: [PATCH 12/25] tons of stuff --- agents/student_agent.py | 331 +++++++++++++++------------------------- agents/test_agent.py | 2 +- play.py | 4 +- simulator.py | 1 + 4 files changed, 128 insertions(+), 210 deletions(-) diff --git a/agents/student_agent.py b/agents/student_agent.py index ae9a451..b721e5a 100644 --- a/agents/student_agent.py +++ b/agents/student_agent.py @@ -24,30 +24,25 @@ def __init__(self): "d": 2, "l": 3, } + self.moves = ((-1, 0), (0, 1), (1, 0), (0, -1)) # moves as defined in world.py (useful for reusing world.py code) + self.opposites = {0: 2, 1: 3, 2: 0, 3: 1} # opposite directions as defined in world.py self.autoplay = True # functions set_barrier(), check_valid_step(), check_endgame() below copied from world.py: - def set_barrier(self, chess_board, r, c, dir): + def set_barrier(self, chess_board, r, c, dir): # adapted from world.py function of same name # Set the barrier to True chess_board[r, c, dir] = True - - moves = ((-1, 0), (0, 1), (1, 0), (0, -1)) - opposites = {0: 2, 1: 3, 2: 0, 3: 1} - # Set the opposite barrier to True - move = moves[dir] - chess_board[r + move[0], c + move[1], opposites[dir]] = True + move = self.moves[dir] + chess_board[r + move[0], c + move[1], self.opposites[dir]] = True def unset_barrier(self, chess_board, r, c, dir): # Set the barrier to False chess_board[r, c, dir] = False - moves = ((-1, 0), (0, 1), (1, 0), (0, -1)) - opposites = {0: 2, 1: 3, 2: 0, 3: 1} - # Set the opposite barrier to False - move = moves[dir] - chess_board[r + move[0], c + move[1], opposites[dir]] = False + move = self.moves[dir] + chess_board[r + move[0], c + move[1], self.opposites[dir]] = False def check_valid_step(self, chess_board, my_start_pos, my_end_pos, barrier_dir, adv_pos, max_step): """ @@ -62,10 +57,7 @@ def check_valid_step(self, chess_board, my_start_pos, my_end_pos, barrier_dir, a barrier_dir : int The direction of the barrier. """ - moves = ((-1, 0), (0, 1), (1, 0), (0, -1)) - # Endpoint already has barrier or is boarder - print(my_end_pos) r, c = my_end_pos if chess_board[r, c, barrier_dir]: return False @@ -83,11 +75,11 @@ def check_valid_step(self, chess_board, my_start_pos, my_end_pos, barrier_dir, a r, c = cur_pos if cur_step == max_step: break - for dir, move in enumerate(moves): + for dir, move in enumerate(self.moves): if chess_board[r, c, dir]: continue - next_pos = cur_pos + move + next_pos = (cur_pos[0] + move[0],cur_pos[1]+move[1]) if np.array_equal(next_pos, adv_pos) or tuple(next_pos) in visited: continue if np.array_equal(next_pos, my_end_pos): @@ -99,7 +91,7 @@ def check_valid_step(self, chess_board, my_start_pos, my_end_pos, barrier_dir, a return is_reached - def check_endgame(self, chess_board, my_pos, adv_pos): + def check_endgame(self, chess_board, my_pos, adv_pos,board_size): """ Check if the game ends and compute the current score of the agents. @@ -112,9 +104,6 @@ def check_endgame(self, chess_board, my_pos, adv_pos): player_2_score : int The score of player 2. """ - moves = ((-1, 0), (0, 1), (1, 0), (0, -1)) - board_size = chess_board.shape[1] # array3d.shape ---> (layer,row,column) - # Union-Find father = dict() for r in range(board_size): @@ -132,7 +121,7 @@ def union(pos1, pos2): for r in range(board_size): for c in range(board_size): for dir, move in enumerate( - moves[1:3] + self.moves[1:3] ): # Only check down and right if chess_board[r, c, dir + 1]: continue @@ -187,210 +176,112 @@ def step(self, chess_board, my_pos, adv_pos, max_step): Please check the sample implementation in agents/random_agent.py or agents/human_agent.py for more details. """ - moves = ((-1, 0), (0, 1), (1, 0), (0, -1)) - - best_score = 0 - best_move = (-1,-1) - - my_new_pos = (-1,-1) - dir_wall = -1 - - # Stores all previous moves that we have searched - previous_moves = [((-1,-1), -1)] - - for step_size in range(1, max_step+1): # eg. when moving 4 steps - for _ in range (0, 3): # we make this number of different moves for our agent in this step_size - # eg. we take 2 different 4-step moves of the agent as a possibility to search - for each_step in range (1, step_size+1): # move to each square... - - # decide a direction to move in - dir_move = np.random.randint(0, 4) - - # we place a wall only on the last step: - if each_step == step_size: - dir_wall = np.random.randint(0,4) - - # Go to a new position - r,c = my_pos - rd,cd = moves[dir_move] - my_new_pos = (r+rd, c+cd) - - while not self.check_valid_step(chess_board, my_pos, my_new_pos, dir_wall, adv_pos, max_step): - - # generate a valid next step: - dir_move = np.random.randint(0,4) - - # we place a wall only on the last step: - if each_step == step_size: - dir_wall = np.random.randint(0,4) - - # If we have already checked this move, do new move... (back to while) - if (dir_move, dir_wall) in previous_moves: - dir_move = (-1,-1) - dir_wall = -1 - r,c = my_pos - rd,cd = moves[dir_move] - my_new_pos = (r+rd, c+cd) - - # If we are at the last step - if each_step == step_size: - r, c = my_new_pos - self.set_barrier(chess_board, r, c, dir_wall) # and unset_barrier at the same position later - # Add this move to the move history - previous_moves = previous_moves + [(my_new_pos, dir_wall)] - - # a new move of step-size steps has been generated here--- - - # Run the minimax algo to check where to place our agent is the best - # this is the minimizing node - score = self.minimax(chess_board, my_new_pos, adv_pos, max_step, False, 10) + board_size = chess_board.shape[1] + + best_score = -1 + + candidate_steps = self.get_valid_steps(chess_board,my_pos,adv_pos,max_step,board_size) + + for step in candidate_steps: + # a new move of step-size steps has been generated here--- + r,c,dir = (step[0],step[1],step[2]) + + self.set_barrier(chess_board,r,c,dir) + # Run the minimax algo to check where to place our agent is the best + # this is the minimizing node + score = self.minimax(chess_board, (r,c), adv_pos, max_step, board_size, False, 3) + + self.unset_barrier(chess_board, r, c, dir) - # Get the optimal ? position and wall direction - r, c = my_new_pos - self.unset_barrier(chess_board, r, c, dir_wall) # ??? this is not right - if score > best_score: - best_score = score - best_move = my_new_pos - best_dir_wall = dir_wall + if score == 1: + best_move = ((r,c),dir) + break + elif score > best_score: + best_move = ((r,c),dir) - return best_move, best_dir_wall + return best_move - def minimax(self, chess_board, my_pos, adv_pos, max_step, is_maximizing, depth): + def minimax(self, chess_board, my_pos, adv_pos, max_step, board_size, is_maximizing, depth): """ is_maximizing is a bool indicating whether we are at maximizing or minimizing step depth is the depth of search + + Returns + score: int + 1 -> max player wins + 0.75 -> depth limit reached + 0.5 -> draw + 0 -> min player wins + """ # Check base cases: - is_end, s1, s2 = self.check_endgame(chess_board, my_pos, adv_pos) + is_end, s1, s2 = self.check_endgame(chess_board, my_pos, adv_pos,board_size) - # if a draw: - if is_end & s1==s2: - return 0.5 + # if a draw or depth limit reached: - # if not draw: - # if the max player wins - if is_end & (not is_maximizing): return 1 - # if the min player wins - elif is_end & is_maximizing: return 0 + if is_end: + if s1 > s2: return 1 + elif s1 < s2: return 0 + else: return 0.5 + + if depth == 0: # if depth limit reached + return 0.75 - moves = ((-1, 0), (0, 1), (1, 0), (0, -1)) best_score = 0 - my_new_pos = (-1,-1) - dir_wall = -1 + # Stores all previous moves that we searched in each for loop - previous_moves = [((-1,-1), -1)] + previous_moves = [(-1,-1,-1)] if is_maximizing: - for step_size in range(1, max_step+1): # eg. when moving 4 steps - for _ in range (0, 3): # we make this number of different moves for our agent in this step_size - # eg. we take 2 different 4-step moves of the agent as a possibility to search - for each_step in range (1, step_size+1): # move to each square... - - # decide a direction to move in - dir_move = np.random.randint(0, 4) - - # we place a wall only on the last step: - if each_step == step_size: - dir_wall = np.random.randint(0,4) - - # Go to a new position - r,c = my_pos - rd,cd = moves[dir_move] - my_new_pos = (r+rd, c+cd) - while not self.check_valid_step(chess_board, my_pos, my_new_pos, dir_wall, adv_pos, max_step): - - # generate a valid next step: - dir_move = np.random.randint(0,4) - - # we place a wall only on the last step: - if each_step == step_size: - dir_wall = np.random.randint(0,4) - - # If we have already checked this move, do new move... (back to while) - if (dir_move, dir_wall) in previous_moves: - dir_move = (-1,-1) - dir_wall = -1 - r,c = my_pos - rd,cd = moves[dir_move] - my_new_pos = (r+rd, c+cd) - - # If we are at the last step - if each_step == step_size: - r, c = my_new_pos - self.set_barrier(chess_board, r, c, dir_wall) # and unset_barrier at the same position later - # Add this move to the move history - previous_moves = previous_moves + [(my_new_pos, dir_wall)] - - # a new move of step-size steps has been generated here--- - - # Run the minimax algo to check where to place our agent is the best - score = self.minimax(chess_board, my_new_pos, adv_pos, max_step, False, depth+1) - - # Get the optimal ? position and wall direction - r, c = my_new_pos - self.unset_barrier(chess_board, r, c, dir_wall) - if score > best_score: - best_score = score - return best_score + candidate_steps = self.get_valid_steps(chess_board,my_pos,adv_pos,max_step,board_size) + + for step in candidate_steps: # move to each square... + r,c,dir = (step[0],step[1],step[2]) + self.set_barrier(chess_board, r, c, dir) # and unset_barrier at the same position later + # Add this move to the move history + previous_moves.append(step) + + + # Run the minimax algo to check where to place our agent is the best + score = self.minimax(chess_board, (r,c), adv_pos, max_step, board_size, False, depth-1) + # end_found, result, = self.minimax(...) + + self.unset_barrier(chess_board, r, c, dir) + + if score == 1: + best_score = 1 + break + elif score > best_score: + best_score = score + + + return score - else: # if not is_maximizing: - - for step_size in range(1, max_step+1): # eg. when moving 4 steps - for _ in range (0, 3): # we make this number of different moves for our agent in this step_size - # eg. we take 2 different 4-step moves of the agent as a possibility to search - for each_step in range (1, step_size+1): # move to each square... - - # decide a direction to move in - dir_move = np.random.randint(0, 4) - - # we place a wall only on the last step: - if each_step == step_size: - dir_wall = np.random.randint(0,4) - - # Go to a new position - r,c = adv_pos - rd,cd = moves[dir_move] - adv_new_pos = (r+rd, c+cd) - # Now we are taking a step for the adversary... - # The arguments are pos, new_pos, ..., adv_pos, - while not self.check_valid_step(chess_board, adv_pos, adv_new_pos, dir_wall, my_pos, max_step): - - # generate a valid next step: - dir_move = np.random.randint(0,4) - - # we place a wall only on the last step: - if each_step == step_size: - dir_wall = np.random.randint(0,4) - - # If we have already checked this move, do new move... (back to while) - if (dir_move, dir_wall) in previous_moves: - dir_move = (-1,-1) - dir_wall = -1 - r,c = adv_pos - rd,cd = moves[dir_move] - adv_new_pos = (r+rd, c+cd) - - # If we are at the last step - if each_step == step_size: - r, c = adv_new_pos - self.set_barrier(chess_board, r, c, dir_wall) # and unset_barrier at the same position later - # Add this move to the move history - previous_moves = previous_moves + [(adv_new_pos, dir_wall)] - - # a new move of step-size steps has been generated here--- - - # Run the minimax algo to check where to place our agent is the best - score = self.minimax(chess_board, my_pos, adv_new_pos, max_step, True, depth+1) # the TRUE here - - # Get the optimal ? position and wall direction - r, c = adv_new_pos - self.unset_barrier(chess_board, r, c, dir_wall) - - if score < best_score: # the LESS THAN here - best_score = score + else: # if not is_maximizing: (it is the adversary's turn) + + candidate_steps = self.get_valid_steps(chess_board,adv_pos,my_pos,max_step,board_size) + + for step in candidate_steps: # move to each square... + r,c,dir = (step[0],step[1],step[2]) + self.set_barrier(chess_board, r, c, dir) # and unset_barrier at the same position later + # Add this move to the move history + previous_moves.append(step) + + + # Run the minimax algo to check where to place our agent is the best + score = self.minimax(chess_board, my_pos, (r,c), max_step, board_size, True, depth-1) + # end_found, result, = self.minimax(...) + + self.unset_barrier(chess_board, r, c, dir) + + if score == 0: + best_score = 0 + break + elif score < best_score: + best_score = score return best_score # M x M board. max_step = floor((M+1)/2) @@ -399,5 +290,29 @@ def minimax(self, chess_board, my_pos, adv_pos, max_step, is_maximizing, depth): # to reduce the branching factor of the search tree but still cover each possible distance, # we can calculate only 2 positions for each number <= max_step, and 4 positions for the wall. - + def get_valid_steps(self, chess_board, my_pos, adv_pos, max_step, board_size): # returns set (can change to list if necessary) of valid steps from current position + end_posits = [my_pos] + valid_steps = [] + for n in range(1,max_step+1): + for r_dist in range(0,n+1): + c_dist = n - r_dist + if r_dist == 0: + cur_steps = [(my_pos[0],my_pos[1] + c_dist),(my_pos[0],my_pos[1] - c_dist)] + elif c_dist == 0: + cur_steps = [(my_pos[0] + r_dist,my_pos[1]),(my_pos[0] - r_dist,my_pos[1])] + else: + cur_steps = [(my_pos[0] + r_dist,my_pos[1] + c_dist),( + my_pos[0] + r_dist,my_pos[1] - c_dist),( + my_pos[0] - r_dist,my_pos[1] + c_dist),( + my_pos[0] - r_dist,my_pos[1] - c_dist)] + end_posits.extend(cur_steps) + # filter steps which leave boundary or end in adversary's location + end_posits = set(filter(lambda end_pos: end_pos[0] < board_size and end_pos[1] < board_size and end_pos[0] >= 0 and end_pos[1] >= 0 and end_pos != adv_pos, end_posits)) + for end_pos in end_posits: + for dir in range(0,4): + if chess_board[end_pos[0],end_pos[1],dir]: + continue + valid_steps.append(tuple((end_pos[0],end_pos[1],dir))) + valid_steps = set(filter(lambda step: self.check_valid_step(chess_board, my_pos,[step[0],step[1]],step[2], adv_pos, max_step),valid_steps)) + return valid_steps \ No newline at end of file diff --git a/agents/test_agent.py b/agents/test_agent.py index 3ca2648..ee4024f 100644 --- a/agents/test_agent.py +++ b/agents/test_agent.py @@ -39,7 +39,7 @@ def step(self, chess_board, my_pos, adv_pos, max_step): print(terminal_steps) print("Stupid Steps:") print(stupid_steps) - not_stupid_steps = self.filter_stupid_steps(chess_board,valid_steps,adv_pos,max_step) + not_stupid_steps = self.get_stupid_steps(chess_board,valid_steps,adv_pos,max_step) print(not_stupid_steps) text = input("Your move (x,y,dir) or input q to quit: ") diff --git a/play.py b/play.py index 7fd3a81..8dc14ef 100644 --- a/play.py +++ b/play.py @@ -1,10 +1,12 @@ import simulator args = simulator.get_args() -args.player_1 = "test_agent" +args.player_1 = "student_agent" args.player_2 = "random_agent" args.display = True args.board_size = 5 +args.autoplay = True +args.autoplay_runs = 10 s1 = simulator.Simulator(args) result = s1.run() diff --git a/simulator.py b/simulator.py index b6a1af9..d88bc2b 100644 --- a/simulator.py +++ b/simulator.py @@ -94,6 +94,7 @@ def autoplay(self): """ Run multiple simulations of the gameplay and aggregate win % """ + args = get_args() p1_win_count = 0 p2_win_count = 0 p1_times = [] From 79b5086add07746969c828908c53cc9c72cebc1f Mon Sep 17 00:00:00 2001 From: cWetaski Date: Thu, 7 Apr 2022 16:39:43 -0400 Subject: [PATCH 13/25] Added filtering of stupid steps, added depth return value to prefer losses/wins further away --- agents/student_agent.py | 117 +++++++++++++++++++++++++++++++--------- 1 file changed, 92 insertions(+), 25 deletions(-) diff --git a/agents/student_agent.py b/agents/student_agent.py index b721e5a..5c61b4a 100644 --- a/agents/student_agent.py +++ b/agents/student_agent.py @@ -44,7 +44,7 @@ def unset_barrier(self, chess_board, r, c, dir): move = self.moves[dir] chess_board[r + move[0], c + move[1], self.opposites[dir]] = False - def check_valid_step(self, chess_board, my_start_pos, my_end_pos, barrier_dir, adv_pos, max_step): + def check_valid_step(self, chess_board, my_start_pos, my_end_pos, dir, adv_pos, max_step): """ Check if the step the agent takes is valid (reachable and within max steps). @@ -54,12 +54,12 @@ def check_valid_step(self, chess_board, my_start_pos, my_end_pos, barrier_dir, a The start position of the agent. end_pos : np.ndarray The end position of the agent. - barrier_dir : int + dir : int The direction of the barrier. """ # Endpoint already has barrier or is boarder r, c = my_end_pos - if chess_board[r, c, barrier_dir]: + if chess_board[r, c, dir]: return False if np.array_equal(my_start_pos, my_end_pos): return True @@ -176,11 +176,26 @@ def step(self, chess_board, my_pos, adv_pos, max_step): Please check the sample implementation in agents/random_agent.py or agents/human_agent.py for more details. """ + depth_limit = 3 # set the depth limit + board_size = chess_board.shape[1] best_score = -1 - - candidate_steps = self.get_valid_steps(chess_board,my_pos,adv_pos,max_step,board_size) + best_depth = depth_limit + + valid_steps = self.get_valid_steps(chess_board,my_pos,adv_pos,max_step,board_size) + # filter stupid steps + stupid_steps_0 = set(filter(lambda step: self.check_stupid_step_0(chess_board,step),valid_steps)) # depth 0 stupid moves + stupid_steps_1 = set(filter(lambda step: self.check_stupid_step_1(chess_board,step,adv_pos,max_step),valid_steps)) # depth 1 stupid moves + candidate_steps = valid_steps - (stupid_steps_0 | stupid_steps_1) + + if len(candidate_steps) == 0: + if(len(stupid_steps_1 > 0)): + best_move = stupid_steps_1.pop() + else: + best_move = stupid_steps_0.pop() + + for step in candidate_steps: # a new move of step-size steps has been generated here--- @@ -189,7 +204,7 @@ def step(self, chess_board, my_pos, adv_pos, max_step): self.set_barrier(chess_board,r,c,dir) # Run the minimax algo to check where to place our agent is the best # this is the minimizing node - score = self.minimax(chess_board, (r,c), adv_pos, max_step, board_size, False, 3) + score, ret_depth = self.minimax(chess_board, (r,c), adv_pos, max_step, board_size, False, depth_limit) self.unset_barrier(chess_board, r, c, dir) @@ -198,6 +213,10 @@ def step(self, chess_board, my_pos, adv_pos, max_step): break elif score > best_score: best_move = ((r,c),dir) + best_depth = ret_depth + elif score == best_score and ret_depth < best_depth: # losing or drawing closer to 0 depth (bottom of search tree) is preferred + best_move = ((r,c),dir) + best_depth = ret_depth return best_move @@ -212,6 +231,7 @@ def minimax(self, chess_board, my_pos, adv_pos, max_step, board_size, is_maximiz 0.75 -> depth limit reached 0.5 -> draw 0 -> min player wins + depth: depth reached for result (if we detect) """ # Check base cases: @@ -220,69 +240,82 @@ def minimax(self, chess_board, my_pos, adv_pos, max_step, board_size, is_maximiz # if a draw or depth limit reached: if is_end: - if s1 > s2: return 1 - elif s1 < s2: return 0 - else: return 0.5 + if s1 > s2: return 1,depth + elif s1 < s2: return 0,depth + else: return 0.5,depth if depth == 0: # if depth limit reached - return 0.75 + return 0.75,depth best_score = 0 - + best_depth = depth # Stores all previous moves that we searched in each for loop - previous_moves = [(-1,-1,-1)] + # previous_moves = [(-1,-1,-1)] if is_maximizing: - candidate_steps = self.get_valid_steps(chess_board,my_pos,adv_pos,max_step,board_size) + valid_steps = self.get_valid_steps(chess_board,my_pos,adv_pos,max_step,board_size) + # filter stupid steps + stupid_steps_0 = set(filter(lambda step: self.check_stupid_step_0(chess_board,step),valid_steps)) # depth 0 stupid moves + stupid_steps_1 = set(filter(lambda step: self.check_stupid_step_1(chess_board,step,adv_pos,max_step),valid_steps)) # depth 1 stupid moves + candidate_steps = valid_steps - (stupid_steps_0 | stupid_steps_1) for step in candidate_steps: # move to each square... r,c,dir = (step[0],step[1],step[2]) self.set_barrier(chess_board, r, c, dir) # and unset_barrier at the same position later # Add this move to the move history - previous_moves.append(step) + # previous_moves.append(step) # Run the minimax algo to check where to place our agent is the best - score = self.minimax(chess_board, (r,c), adv_pos, max_step, board_size, False, depth-1) + score,ret_depth = self.minimax(chess_board, (r,c), adv_pos, max_step, board_size, False, depth-1) # end_found, result, = self.minimax(...) self.unset_barrier(chess_board, r, c, dir) if score == 1: best_score = 1 + best_depth = ret_depth break elif score > best_score: best_score = score - - - return score + best_depth = ret_depth + elif score == best_score and ret_depth < best_depth: + best_depth = ret_depth + return best_score,best_depth else: # if not is_maximizing: (it is the adversary's turn) - candidate_steps = self.get_valid_steps(chess_board,adv_pos,my_pos,max_step,board_size) - + valid_steps = self.get_valid_steps(chess_board,my_pos,adv_pos,max_step,board_size) + # filter stupid steps + stupid_steps_0 = set(filter(lambda step: self.check_stupid_step_0(chess_board,step),valid_steps)) # depth 0 stupid moves + stupid_steps_1 = set(filter(lambda step: self.check_stupid_step_1(chess_board,step,my_pos,max_step),valid_steps)) # depth 1 stupid moves + candidate_steps = valid_steps - (stupid_steps_0 | stupid_steps_1) for step in candidate_steps: # move to each square... r,c,dir = (step[0],step[1],step[2]) self.set_barrier(chess_board, r, c, dir) # and unset_barrier at the same position later # Add this move to the move history - previous_moves.append(step) + # previous_moves.append(step) # Run the minimax algo to check where to place our agent is the best - score = self.minimax(chess_board, my_pos, (r,c), max_step, board_size, True, depth-1) + score,ret_depth = self.minimax(chess_board, my_pos, (r,c), max_step, board_size, True, depth-1) # end_found, result, = self.minimax(...) self.unset_barrier(chess_board, r, c, dir) if score == 0: best_score = 0 + best_depth = ret_depth break - elif score < best_score: + elif score > best_score: # technically means the opposing agent prefers a draw even tho we prefer a win best_score = score - return best_score + best_depth = ret_depth + elif score == best_score and ret_depth < best_depth: + best_depth = ret_depth + return best_score,depth # M x M board. max_step = floor((M+1)/2) # for each node in the search tree, it has children for every possible step number : @@ -314,5 +347,39 @@ def get_valid_steps(self, chess_board, my_pos, adv_pos, max_step, board_size): # continue valid_steps.append(tuple((end_pos[0],end_pos[1],dir))) valid_steps = set(filter(lambda step: self.check_valid_step(chess_board, my_pos,[step[0],step[1]],step[2], adv_pos, max_step),valid_steps)) - return valid_steps + return valid_steps + + def get_empty_edges(self,chess_board,pos): + empty_edges = [] + for dir in range(0,4): + if not (chess_board[pos[0],pos[1],dir]): + empty_edges.append(dir) + return empty_edges + + def check_stupid_step_0(self,chess_board,step): # A 0-stupid_step makes us lose this turn + """ + This is not exhaustive of moves in which make us lose + Checks for if move puts us in a 1x1 box, forcing us to lose + """ + r,c = (step[0],step[1]) + empty_edges = self.get_empty_edges(chess_board,(r,c)) + if (len(empty_edges) == 1): + return True + return False + + def check_stupid_step_1(self, chess_board,step,adv_pos,max_step): # A 1-stupid step allows the opponent to beat us next turn + """ + This is not exhaustive of moves in which we can lose next turn + Checks for if move puts us in a box with 3 edges around it where the opponent can reach the empty edge in their next turn + """ + r,c,dir = step + empty_edges = self.get_empty_edges(chess_board,(r,c)) + if (len(empty_edges) == 2): # might be stupid + empty_edges.remove(dir) + empty_dir = empty_edges[0] + move = self.moves[empty_dir] + win_pos = (r+move[0],c+move[1]) + if self.check_valid_step(chess_board,adv_pos,win_pos,self.opposites[empty_dir],(r,c),max_step): + return True + return False \ No newline at end of file From f4eb3f95ffd7d90c2782d6aedbc361296ce64e1b Mon Sep 17 00:00:00 2001 From: cWetaski Date: Thu, 7 Apr 2022 17:23:32 -0400 Subject: [PATCH 14/25] added weighted score (and bugfixes in minimax) --- agents/student_agent.py | 46 +++++++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/agents/student_agent.py b/agents/student_agent.py index 5c61b4a..3ca590f 100644 --- a/agents/student_agent.py +++ b/agents/student_agent.py @@ -204,20 +204,27 @@ def step(self, chess_board, my_pos, adv_pos, max_step): self.set_barrier(chess_board,r,c,dir) # Run the minimax algo to check where to place our agent is the best # this is the minimizing node - score, ret_depth = self.minimax(chess_board, (r,c), adv_pos, max_step, board_size, False, depth_limit) + score, ret_depth,results = self.minimax(chess_board, (r,c), adv_pos, max_step, board_size, False, depth_limit) self.unset_barrier(chess_board, r, c, dir) - + + weighted_score = (results[0]-results[1])/(results[2]) + if score == 1: best_move = ((r,c),dir) break elif score > best_score: best_move = ((r,c),dir) best_depth = ret_depth + best_weighted_score = weighted_score elif score == best_score and ret_depth < best_depth: # losing or drawing closer to 0 depth (bottom of search tree) is preferred best_move = ((r,c),dir) best_depth = ret_depth - + best_weighted_score + elif score == best_score and ret_depth == best_depth and weighted_score > best_weighted_score: # a move which has a higher weighted score is better, all other things equal + best_move = ((r,c),dir) + best_depth = ret_depth + best_weighted_score return best_move def minimax(self, chess_board, my_pos, adv_pos, max_step, board_size, is_maximizing, depth): @@ -232,20 +239,26 @@ def minimax(self, chess_board, my_pos, adv_pos, max_step, board_size, is_maximiz 0.5 -> draw 0 -> min player wins depth: depth reached for result (if we detect) - + results: [int,int,int] + array of results discovered (wins-draws-losses) """ # Check base cases: is_end, s1, s2 = self.check_endgame(chess_board, my_pos, adv_pos,board_size) # if a draw or depth limit reached: - + results = np.array([0,0,1]) # wins, losses, moves (don't care about draws for this) + if is_end: - if s1 > s2: return 1,depth - elif s1 < s2: return 0,depth - else: return 0.5,depth + if s1 > s2: + results[0]=1 + return 1,depth,results + elif s1 < s2: + results[1]=1 + return 0,depth,results + else: return 0.5,depth,results if depth == 0: # if depth limit reached - return 0.75,depth + return 0.75,depth,results best_score = 0 best_depth = depth @@ -270,11 +283,11 @@ def minimax(self, chess_board, my_pos, adv_pos, max_step, board_size, is_maximiz # Run the minimax algo to check where to place our agent is the best - score,ret_depth = self.minimax(chess_board, (r,c), adv_pos, max_step, board_size, False, depth-1) + score,ret_depth,ret_results = self.minimax(chess_board, (r,c), adv_pos, max_step, board_size, False, depth-1) # end_found, result, = self.minimax(...) - + results = np.add(results,ret_results) # add results which were found self.unset_barrier(chess_board, r, c, dir) - + if score == 1: best_score = 1 best_depth = ret_depth @@ -284,7 +297,8 @@ def minimax(self, chess_board, my_pos, adv_pos, max_step, board_size, is_maximiz best_depth = ret_depth elif score == best_score and ret_depth < best_depth: best_depth = ret_depth - return best_score,best_depth + + return best_score,best_depth,results else: # if not is_maximizing: (it is the adversary's turn) @@ -301,9 +315,9 @@ def minimax(self, chess_board, my_pos, adv_pos, max_step, board_size, is_maximiz # Run the minimax algo to check where to place our agent is the best - score,ret_depth = self.minimax(chess_board, my_pos, (r,c), max_step, board_size, True, depth-1) + score,ret_depth,ret_results = self.minimax(chess_board, my_pos, (r,c), max_step, board_size, True, depth-1) # end_found, result, = self.minimax(...) - + results = np.add(results,ret_results) self.unset_barrier(chess_board, r, c, dir) if score == 0: @@ -315,7 +329,7 @@ def minimax(self, chess_board, my_pos, adv_pos, max_step, board_size, is_maximiz best_depth = ret_depth elif score == best_score and ret_depth < best_depth: best_depth = ret_depth - return best_score,depth + return best_score,depth,results # M x M board. max_step = floor((M+1)/2) # for each node in the search tree, it has children for every possible step number : From 2a36c210818fc94706142041d48aaa1ffcf251e9 Mon Sep 17 00:00:00 2001 From: cWetaski Date: Thu, 7 Apr 2022 20:35:58 -0400 Subject: [PATCH 15/25] greatly improved process of getting valid steps --- agents/student_agent.py | 97 +++++++++++++++++++++++++++-------------- play.py | 5 ++- simulator.py | 4 +- 3 files changed, 72 insertions(+), 34 deletions(-) diff --git a/agents/student_agent.py b/agents/student_agent.py index 3ca590f..e753200 100644 --- a/agents/student_agent.py +++ b/agents/student_agent.py @@ -6,7 +6,7 @@ import sys import numpy as np - +import time @register_agent("student_agent") class StudentAgent(Agent): @@ -27,6 +27,9 @@ def __init__(self): self.moves = ((-1, 0), (0, 1), (1, 0), (0, -1)) # moves as defined in world.py (useful for reusing world.py code) self.opposites = {0: 2, 1: 3, 2: 0, 3: 1} # opposite directions as defined in world.py self.autoplay = True + self.check_endgame_timer = 0 + self.check_step_timer = 0 + self.first_turn = True # functions set_barrier(), check_valid_step(), check_endgame() below copied from world.py: def set_barrier(self, chess_board, r, c, dir): # adapted from world.py function of same name @@ -57,6 +60,7 @@ def check_valid_step(self, chess_board, my_start_pos, my_end_pos, dir, adv_pos, dir : int The direction of the barrier. """ + # Endpoint already has barrier or is boarder r, c = my_end_pos if chess_board[r, c, dir]: @@ -88,7 +92,6 @@ def check_valid_step(self, chess_board, my_start_pos, my_end_pos, dir, adv_pos, visited.add(tuple(next_pos)) state_queue.append((next_pos, cur_step + 1)) - return is_reached def check_endgame(self, chess_board, my_pos, adv_pos,board_size): @@ -104,6 +107,7 @@ def check_endgame(self, chess_board, my_pos, adv_pos,board_size): player_2_score : int The score of player 2. """ + start = time.time() # Union-Find father = dict() for r in range(board_size): @@ -158,6 +162,8 @@ def union(pos1, pos2): else: logging.info("Game ends! It is a Tie!") ''' + end = time.time() + self.endgame_timer = self.endgame_timer + (end - start) return True, p0_score, p1_score # THE ACTAUL IMPLEMENTATION... @@ -176,14 +182,18 @@ def step(self, chess_board, my_pos, adv_pos, max_step): Please check the sample implementation in agents/random_agent.py or agents/human_agent.py for more details. """ - depth_limit = 3 # set the depth limit + depth_limit = 2 # set the depth limit + time_limit = 20 + self.endgame_timer = 0 + self.check_step_timer = 0 + start = time.time() board_size = chess_board.shape[1] best_score = -1 best_depth = depth_limit - valid_steps = self.get_valid_steps(chess_board,my_pos,adv_pos,max_step,board_size) + valid_steps = self.get_valid_steps(chess_board,my_pos,adv_pos,max_step) # filter stupid steps stupid_steps_0 = set(filter(lambda step: self.check_stupid_step_0(chess_board,step),valid_steps)) # depth 0 stupid moves stupid_steps_1 = set(filter(lambda step: self.check_stupid_step_1(chess_board,step,adv_pos,max_step),valid_steps)) # depth 1 stupid moves @@ -194,10 +204,11 @@ def step(self, chess_board, my_pos, adv_pos, max_step): best_move = stupid_steps_1.pop() else: best_move = stupid_steps_0.pop() - - - + print("") for step in candidate_steps: + if time.time() - start > time_limit: + print('time limit exceeded') + break # a new move of step-size steps has been generated here--- r,c,dir = (step[0],step[1],step[2]) @@ -225,6 +236,11 @@ def step(self, chess_board, my_pos, adv_pos, max_step): best_move = ((r,c),dir) best_depth = ret_depth best_weighted_score + if self.first_turn == True: + self.first_turn = False + time_limit = 1.5 + print (time.time() - start) + return best_move def minimax(self, chess_board, my_pos, adv_pos, max_step, board_size, is_maximizing, depth): @@ -269,7 +285,7 @@ def minimax(self, chess_board, my_pos, adv_pos, max_step, board_size, is_maximiz if is_maximizing: - valid_steps = self.get_valid_steps(chess_board,my_pos,adv_pos,max_step,board_size) + valid_steps = self.get_valid_steps(chess_board,my_pos,adv_pos,max_step) # filter stupid steps stupid_steps_0 = set(filter(lambda step: self.check_stupid_step_0(chess_board,step),valid_steps)) # depth 0 stupid moves stupid_steps_1 = set(filter(lambda step: self.check_stupid_step_1(chess_board,step,adv_pos,max_step),valid_steps)) # depth 1 stupid moves @@ -302,7 +318,7 @@ def minimax(self, chess_board, my_pos, adv_pos, max_step, board_size, is_maximiz else: # if not is_maximizing: (it is the adversary's turn) - valid_steps = self.get_valid_steps(chess_board,my_pos,adv_pos,max_step,board_size) + valid_steps = self.get_valid_steps(chess_board,my_pos,adv_pos,max_step) # filter stupid steps stupid_steps_0 = set(filter(lambda step: self.check_stupid_step_0(chess_board,step),valid_steps)) # depth 0 stupid moves stupid_steps_1 = set(filter(lambda step: self.check_stupid_step_1(chess_board,step,my_pos,max_step),valid_steps)) # depth 1 stupid moves @@ -337,32 +353,48 @@ def minimax(self, chess_board, my_pos, adv_pos, max_step, board_size, is_maximiz # to reduce the branching factor of the search tree but still cover each possible distance, # we can calculate only 2 positions for each number <= max_step, and 4 positions for the wall. - def get_valid_steps(self, chess_board, my_pos, adv_pos, max_step, board_size): # returns set (can change to list if necessary) of valid steps from current position + def get_valid_steps(self, chess_board, my_pos, adv_pos, max_step): # returns set (can change to list if necessary) of valid steps from current position + end_posits = [my_pos] - valid_steps = [] - for n in range(1,max_step+1): - for r_dist in range(0,n+1): - c_dist = n - r_dist - if r_dist == 0: - cur_steps = [(my_pos[0],my_pos[1] + c_dist),(my_pos[0],my_pos[1] - c_dist)] - elif c_dist == 0: - cur_steps = [(my_pos[0] + r_dist,my_pos[1]),(my_pos[0] - r_dist,my_pos[1])] - else: - cur_steps = [(my_pos[0] + r_dist,my_pos[1] + c_dist),( - my_pos[0] + r_dist,my_pos[1] - c_dist),( - my_pos[0] - r_dist,my_pos[1] + c_dist),( - my_pos[0] - r_dist,my_pos[1] - c_dist)] - end_posits.extend(cur_steps) - # filter steps which leave boundary or end in adversary's location - end_posits = set(filter(lambda end_pos: end_pos[0] < board_size and end_pos[1] < board_size and end_pos[0] >= 0 and end_pos[1] >= 0 and end_pos != adv_pos, end_posits)) + valid_steps = set() + end_posits = self.search_valid_pos(chess_board,my_pos,adv_pos,max_step) for end_pos in end_posits: - for dir in range(0,4): - if chess_board[end_pos[0],end_pos[1],dir]: - continue - valid_steps.append(tuple((end_pos[0],end_pos[1],dir))) - valid_steps = set(filter(lambda step: self.check_valid_step(chess_board, my_pos,[step[0],step[1]],step[2], adv_pos, max_step),valid_steps)) + empty_edges = self.get_empty_edges(chess_board,end_pos) + for dir in empty_edges: + valid_steps.add((end_pos[0],end_pos[1],dir)) return valid_steps + def search_valid_pos(self, chess_board, my_start_pos, adv_pos, max_step): + """ + Modified version of check_valid_step which returns all reachable positions from a given location with a given max_step + Parameters + ---------- + start_pos : tuple + The start position of the agent. + end_pos : np.ndarray + The end position of the agent. + """ + # BFS + state_queue = [(my_start_pos, 0)] + visited = {tuple(my_start_pos)} + cur_step = 0 + while cur_step < max_step: + cur_pos, cur_step = state_queue.pop(0) + r, c = cur_pos + if cur_step == max_step: + break + for dir, move in enumerate(self.moves): + if chess_board[r, c, dir]: + continue + + next_pos = (cur_pos[0] + move[0],cur_pos[1]+move[1]) + if np.array_equal(next_pos, adv_pos) or tuple(next_pos) in visited: + continue + + visited.add(tuple(next_pos)) + state_queue.append((next_pos, cur_step + 1)) + return visited + def get_empty_edges(self,chess_board,pos): empty_edges = [] for dir in range(0,4): @@ -396,4 +428,5 @@ def check_stupid_step_1(self, chess_board,step,adv_pos,max_step): # A 1-stupid s if self.check_valid_step(chess_board,adv_pos,win_pos,self.opposites[empty_dir],(r,c),max_step): return True return False - \ No newline at end of file + + \ No newline at end of file diff --git a/play.py b/play.py index 8dc14ef..6af8218 100644 --- a/play.py +++ b/play.py @@ -6,8 +6,11 @@ args.display = True args.board_size = 5 args.autoplay = True -args.autoplay_runs = 10 +args.autoplay_runs = 100 +args.board_size_min = 4 +args.board_size_max = 6 s1 = simulator.Simulator(args) +#result = s1.autoplay() result = s1.run() diff --git a/simulator.py b/simulator.py index d88bc2b..d453c8e 100644 --- a/simulator.py +++ b/simulator.py @@ -94,7 +94,7 @@ def autoplay(self): """ Run multiple simulations of the gameplay and aggregate win % """ - args = get_args() + args = self.args p1_win_count = 0 p2_win_count = 0 p1_times = [] @@ -118,7 +118,9 @@ def autoplay(self): ) if p0_score > p1_score: p1_win_count += 1 + print("p1 wins!") elif p0_score < p1_score: + print("p2 wins!") p2_win_count += 1 else: # Tie p1_win_count += 1 From 0166c941591840105bba98d52eba8b2a226e2b06 Mon Sep 17 00:00:00 2001 From: cWetaski Date: Thu, 7 Apr 2022 20:38:59 -0400 Subject: [PATCH 16/25] fixed small bug in previous push --- agents/student_agent.py | 2 +- play.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/agents/student_agent.py b/agents/student_agent.py index e753200..2039a8e 100644 --- a/agents/student_agent.py +++ b/agents/student_agent.py @@ -378,7 +378,7 @@ def search_valid_pos(self, chess_board, my_start_pos, adv_pos, max_step): state_queue = [(my_start_pos, 0)] visited = {tuple(my_start_pos)} cur_step = 0 - while cur_step < max_step: + while state_queue: cur_pos, cur_step = state_queue.pop(0) r, c = cur_pos if cur_step == max_step: diff --git a/play.py b/play.py index 6af8218..93b2cce 100644 --- a/play.py +++ b/play.py @@ -10,8 +10,8 @@ args.board_size_min = 4 args.board_size_max = 6 s1 = simulator.Simulator(args) -#result = s1.autoplay() -result = s1.run() +result = s1.autoplay() +#result = s1.run() From abdff8be811b3c381fb3dc6d9ca33a2fb63ac471 Mon Sep 17 00:00:00 2001 From: cWetaski Date: Thu, 7 Apr 2022 21:45:06 -0400 Subject: [PATCH 17/25] it's working pretty well, trying some time optimization stuff --- agents/student_agent.py | 125 +++++++++++++++++++++------------------- play.py | 7 ++- simulator.py | 4 +- 3 files changed, 71 insertions(+), 65 deletions(-) diff --git a/agents/student_agent.py b/agents/student_agent.py index 2039a8e..e9f22dc 100644 --- a/agents/student_agent.py +++ b/agents/student_agent.py @@ -27,9 +27,9 @@ def __init__(self): self.moves = ((-1, 0), (0, 1), (1, 0), (0, -1)) # moves as defined in world.py (useful for reusing world.py code) self.opposites = {0: 2, 1: 3, 2: 0, 3: 1} # opposite directions as defined in world.py self.autoplay = True - self.check_endgame_timer = 0 - self.check_step_timer = 0 self.first_turn = True + self.timer = 0 + self.time_limit = 25 # functions set_barrier(), check_valid_step(), check_endgame() below copied from world.py: def set_barrier(self, chess_board, r, c, dir): # adapted from world.py function of same name @@ -47,53 +47,6 @@ def unset_barrier(self, chess_board, r, c, dir): move = self.moves[dir] chess_board[r + move[0], c + move[1], self.opposites[dir]] = False - def check_valid_step(self, chess_board, my_start_pos, my_end_pos, dir, adv_pos, max_step): - """ - Check if the step the agent takes is valid (reachable and within max steps). - - Parameters - ---------- - start_pos : tuple - The start position of the agent. - end_pos : np.ndarray - The end position of the agent. - dir : int - The direction of the barrier. - """ - - # Endpoint already has barrier or is boarder - r, c = my_end_pos - if chess_board[r, c, dir]: - return False - if np.array_equal(my_start_pos, my_end_pos): - return True - - # Get position of the adversary... deleted - - # BFS - state_queue = [(my_start_pos, 0)] - visited = {tuple(my_start_pos)} - is_reached = False - while state_queue and not is_reached: - cur_pos, cur_step = state_queue.pop(0) - r, c = cur_pos - if cur_step == max_step: - break - for dir, move in enumerate(self.moves): - if chess_board[r, c, dir]: - continue - - next_pos = (cur_pos[0] + move[0],cur_pos[1]+move[1]) - if np.array_equal(next_pos, adv_pos) or tuple(next_pos) in visited: - continue - if np.array_equal(next_pos, my_end_pos): - is_reached = True - break - - visited.add(tuple(next_pos)) - state_queue.append((next_pos, cur_step + 1)) - return is_reached - def check_endgame(self, chess_board, my_pos, adv_pos,board_size): """ Check if the game ends and compute the current score of the agents. @@ -182,13 +135,15 @@ def step(self, chess_board, my_pos, adv_pos, max_step): Please check the sample implementation in agents/random_agent.py or agents/human_agent.py for more details. """ - depth_limit = 2 # set the depth limit - time_limit = 20 + + board_size = chess_board.shape[1] + depth_limit = 4 + if board_size > 7: + depth_limit = 2 self.endgame_timer = 0 self.check_step_timer = 0 - start = time.time() + self.timer = time.time() - board_size = chess_board.shape[1] best_score = -1 best_depth = depth_limit @@ -196,17 +151,19 @@ def step(self, chess_board, my_pos, adv_pos, max_step): valid_steps = self.get_valid_steps(chess_board,my_pos,adv_pos,max_step) # filter stupid steps stupid_steps_0 = set(filter(lambda step: self.check_stupid_step_0(chess_board,step),valid_steps)) # depth 0 stupid moves + candidate_steps = valid_steps - stupid_steps_0 + board_size <= 7 stupid_steps_1 = set(filter(lambda step: self.check_stupid_step_1(chess_board,step,adv_pos,max_step),valid_steps)) # depth 1 stupid moves - candidate_steps = valid_steps - (stupid_steps_0 | stupid_steps_1) + candidate_steps = valid_steps - stupid_steps_1 + if len(candidate_steps) == 0: if(len(stupid_steps_1 > 0)): best_move = stupid_steps_1.pop() else: best_move = stupid_steps_0.pop() - print("") for step in candidate_steps: - if time.time() - start > time_limit: + if (time.time() - self.timer) > self.time_limit: print('time limit exceeded') break # a new move of step-size steps has been generated here--- @@ -238,9 +195,9 @@ def step(self, chess_board, my_pos, adv_pos, max_step): best_weighted_score if self.first_turn == True: self.first_turn = False - time_limit = 1.5 - print (time.time() - start) - + self.time_limit = 1.8 + if (time.time() - self.timer) > self.time_limit: + print(time.time() - self.timer) return best_move def minimax(self, chess_board, my_pos, adv_pos, max_step, board_size, is_maximizing, depth): @@ -292,6 +249,8 @@ def minimax(self, chess_board, my_pos, adv_pos, max_step, board_size, is_maximiz candidate_steps = valid_steps - (stupid_steps_0 | stupid_steps_1) for step in candidate_steps: # move to each square... + if (time.time() - self.timer) > self.time_limit: + break r,c,dir = (step[0],step[1],step[2]) self.set_barrier(chess_board, r, c, dir) # and unset_barrier at the same position later # Add this move to the move history @@ -428,5 +387,53 @@ def check_stupid_step_1(self, chess_board,step,adv_pos,max_step): # A 1-stupid s if self.check_valid_step(chess_board,adv_pos,win_pos,self.opposites[empty_dir],(r,c),max_step): return True return False + + def check_valid_step(self, chess_board, my_start_pos, my_end_pos, dir, adv_pos, max_step): + """ + Check if the step the agent takes is valid (reachable and within max steps). + + Parameters + ---------- + start_pos : tuple + The start position of the agent. + end_pos : np.ndarray + The end position of the agent. + dir : int + The direction of the barrier. + """ + + # Endpoint already has barrier or is boarder + r, c = my_end_pos + if chess_board[r, c, dir]: + return False + if np.array_equal(my_start_pos, my_end_pos): + return True + + # Get position of the adversary... deleted + + # BFS + state_queue = [(my_start_pos, 0)] + visited = {tuple(my_start_pos)} + is_reached = False + while state_queue and not is_reached: + cur_pos, cur_step = state_queue.pop(0) + r, c = cur_pos + if cur_step == max_step: + break + for dir, move in enumerate(self.moves): + if chess_board[r, c, dir]: + continue + + next_pos = (cur_pos[0] + move[0],cur_pos[1]+move[1]) + if np.array_equal(next_pos, adv_pos) or tuple(next_pos) in visited: + continue + if np.array_equal(next_pos, my_end_pos): + is_reached = True + break + + visited.add(tuple(next_pos)) + state_queue.append((next_pos, cur_step + 1)) + return is_reached + \ No newline at end of file diff --git a/play.py b/play.py index 93b2cce..3fa4027 100644 --- a/play.py +++ b/play.py @@ -6,9 +6,10 @@ args.display = True args.board_size = 5 args.autoplay = True -args.autoplay_runs = 100 -args.board_size_min = 4 -args.board_size_max = 6 +args.autoplay_runs = 10 +args.board_size_min = 9 +args.board_size_max = 10 +args.board_size = 6 s1 = simulator.Simulator(args) result = s1.autoplay() #result = s1.run() diff --git a/simulator.py b/simulator.py index d453c8e..6302f91 100644 --- a/simulator.py +++ b/simulator.py @@ -118,9 +118,7 @@ def autoplay(self): ) if p0_score > p1_score: p1_win_count += 1 - print("p1 wins!") elif p0_score < p1_score: - print("p2 wins!") p2_win_count += 1 else: # Tie p1_win_count += 1 @@ -129,7 +127,7 @@ def autoplay(self): p2_times.append(p1_time) logger.info( - f"Player {PLAYER_1_NAME} win percentage: {p1_win_count / self.args.autoplay_runs} ({np.round(np.mean(p1_times), 5)} seconds/game)" + f"layer {PLAYER_1_NAME} win percentage: {p1_win_count / self.args.autoplay_runs} ({np.round(np.mean(p1_times), 5)} seconds/game)" ) logger.info( f"Player {PLAYER_2_NAME} win percentage: {p2_win_count / self.args.autoplay_runs}, ({np.round(np.mean(p2_times), 5)} seconds/game)" From fa5b02abfd190e8f4219ff4d4494fa8a829f252b Mon Sep 17 00:00:00 2001 From: FFFlora0349 <59624826+FFFlora0349@users.noreply.github.com> Date: Fri, 8 Apr 2022 12:55:09 -0400 Subject: [PATCH 18/25] minor changes --- agents/student_agent.py | 163 ++++++++++++++++++++++------------------ agents/test_agent.py | 26 +++++++ simulator.py | 2 +- 3 files changed, 115 insertions(+), 76 deletions(-) diff --git a/agents/student_agent.py b/agents/student_agent.py index e9f22dc..8200cc6 100644 --- a/agents/student_agent.py +++ b/agents/student_agent.py @@ -29,7 +29,8 @@ def __init__(self): self.autoplay = True self.first_turn = True self.timer = 0 - self.time_limit = 25 + self.time_limit = 28 + self.endgame_timer = 0 #??? # functions set_barrier(), check_valid_step(), check_endgame() below copied from world.py: def set_barrier(self, chess_board, r, c, dir): # adapted from world.py function of same name @@ -42,8 +43,7 @@ def set_barrier(self, chess_board, r, c, dir): # adapted from world.py function def unset_barrier(self, chess_board, r, c, dir): # Set the barrier to False chess_board[r, c, dir] = False - - # Set the opposite barrier to False + # Set the opposite barrier to False move = self.moves[dir] chess_board[r + move[0], c + move[1], self.opposites[dir]] = False @@ -61,6 +61,7 @@ def check_endgame(self, chess_board, my_pos, adv_pos,board_size): The score of player 2. """ start = time.time() + # Union-Find father = dict() for r in range(board_size): @@ -90,7 +91,7 @@ def union(pos1, pos2): for r in range(board_size): for c in range(board_size): find((r, c)) - # + p0_r = find(tuple(my_pos)) p1_r = find(tuple(adv_pos)) p0_score = list(father.values()).count(p0_r) @@ -127,7 +128,7 @@ def step(self, chess_board, my_pos, adv_pos, max_step): - chess_board: a numpy array of shape (x_max, y_max, 4) 3-dimentional - my_pos: a tuple of (x, y) - adv_pos: a tuple of (x, y) - - max_step: an integer + - max_step: an integer # M x M board. max_step = floor((M+1)/2) You should return a tuple of ((x, y), dir), where (x, y) is the next position of your agent and dir is the direction of the wall @@ -141,61 +142,66 @@ def step(self, chess_board, my_pos, adv_pos, max_step): if board_size > 7: depth_limit = 2 self.endgame_timer = 0 - self.check_step_timer = 0 - self.timer = time.time() - + self.check_step_timer = 0 # not used + self.timer = time.time() # record the current time at the start of each step best_score = -1 best_depth = depth_limit + # Heuristics to choose the move with a higher percentage of winning rate + weighted_score = 0 valid_steps = self.get_valid_steps(chess_board,my_pos,adv_pos,max_step) - # filter stupid steps - stupid_steps_0 = set(filter(lambda step: self.check_stupid_step_0(chess_board,step),valid_steps)) # depth 0 stupid moves - candidate_steps = valid_steps - stupid_steps_0 - board_size <= 7 - stupid_steps_1 = set(filter(lambda step: self.check_stupid_step_1(chess_board,step,adv_pos,max_step),valid_steps)) # depth 1 stupid moves - candidate_steps = valid_steps - stupid_steps_1 - + # stupid_steps_0 = set(filter(lambda step: self.check_stupid_step_0(chess_board,step),valid_steps)) # depth 0 stupid moves + # candidate_steps = valid_steps - stupid_steps_0 + # filter depth 0 and 1 stupid moves + stupid_steps = set(filter(lambda step: self.check_stupid_step(chess_board,step,adv_pos,max_step),valid_steps)) + candidate_steps = valid_steps - stupid_steps if len(candidate_steps) == 0: - if(len(stupid_steps_1 > 0)): - best_move = stupid_steps_1.pop() - else: - best_move = stupid_steps_0.pop() + if(len(stupid_steps > 0)): + best_move = stupid_steps.pop() + # else: + # best_move = stupid_steps_0.pop() for step in candidate_steps: if (time.time() - self.timer) > self.time_limit: - print('time limit exceeded') + # print('time limit exceeded') break # a new move of step-size steps has been generated here--- r,c,dir = (step[0],step[1],step[2]) - self.set_barrier(chess_board,r,c,dir) + # Run the minimax algo to check where to place our agent is the best # this is the minimizing node - score, ret_depth,results = self.minimax(chess_board, (r,c), adv_pos, max_step, board_size, False, depth_limit) + score, ret_depth, results = self.minimax(chess_board, (r,c), adv_pos, max_step, board_size, False, depth_limit) self.unset_barrier(chess_board, r, c, dir) weighted_score = (results[0]-results[1])/(results[2]) + best_depth = ret_depth # ADDED + if score == 1: best_move = ((r,c),dir) break elif score > best_score: best_move = ((r,c),dir) - best_depth = ret_depth - best_weighted_score = weighted_score - elif score == best_score and ret_depth < best_depth: # losing or drawing closer to 0 depth (bottom of search tree) is preferred + # best_depth = ret_depth # ADDED ABOVE + # best_weighted_score = weighted_score # don't need it here + # losing or drawing closer to 0 depth (bottom of search tree) is preferred + elif score == best_score and ret_depth < best_depth: best_move = ((r,c),dir) - best_depth = ret_depth - best_weighted_score - elif score == best_score and ret_depth == best_depth and weighted_score > best_weighted_score: # a move which has a higher weighted score is better, all other things equal + # best_depth = ret_depth + # best_weighted_score #??? + # a move which has a higher weighted score is better, all other things equal + elif score == best_score and ret_depth == best_depth and weighted_score > best_weighted_score: best_move = ((r,c),dir) - best_depth = ret_depth - best_weighted_score + # best_depth = ret_depth + best_weighted_score = weighted_score + if self.first_turn == True: self.first_turn = False - self.time_limit = 1.8 + self.time_limit = 1.9 + if (time.time() - self.timer) > self.time_limit: print(time.time() - self.timer) return best_move @@ -228,89 +234,89 @@ def minimax(self, chess_board, my_pos, adv_pos, max_step, board_size, is_maximiz elif s1 < s2: results[1]=1 return 0,depth,results - else: return 0.5,depth,results + else: + return 0.5,depth,results - if depth == 0: # if depth limit reached + if depth == 0: # if depth limit reached (height limit actually) return 0.75,depth,results best_score = 0 best_depth = depth - # Stores all previous moves that we searched in each for loop - # previous_moves = [(-1,-1,-1)] - if is_maximizing: valid_steps = self.get_valid_steps(chess_board,my_pos,adv_pos,max_step) - # filter stupid steps - stupid_steps_0 = set(filter(lambda step: self.check_stupid_step_0(chess_board,step),valid_steps)) # depth 0 stupid moves - stupid_steps_1 = set(filter(lambda step: self.check_stupid_step_1(chess_board,step,adv_pos,max_step),valid_steps)) # depth 1 stupid moves - candidate_steps = valid_steps - (stupid_steps_0 | stupid_steps_1) + #stupid_steps_0 = set(filter(lambda step: self.check_stupid_step_0(chess_board,step),valid_steps)) + # filter depth 0 and 1 stupid moves + stupid_steps = set(filter(lambda step: self.check_stupid_step(chess_board,step,adv_pos,max_step),valid_steps)) + candidate_steps = valid_steps - stupid_steps #(stupid_steps_0 | stupid_steps_1) for step in candidate_steps: # move to each square... if (time.time() - self.timer) > self.time_limit: break + r,c,dir = (step[0],step[1],step[2]) self.set_barrier(chess_board, r, c, dir) # and unset_barrier at the same position later - # Add this move to the move history - # previous_moves.append(step) - # Run the minimax algo to check where to place our agent is the best score,ret_depth,ret_results = self.minimax(chess_board, (r,c), adv_pos, max_step, board_size, False, depth-1) - # end_found, result, = self.minimax(...) - results = np.add(results,ret_results) # add results which were found + self.unset_barrier(chess_board, r, c, dir) + results = np.add(results,ret_results) # add results which were found + best_depth = ret_depth #ADDED + if score == 1: best_score = 1 - best_depth = ret_depth + # best_depth = ret_depth break elif score > best_score: best_score = score - best_depth = ret_depth - elif score == best_score and ret_depth < best_depth: - best_depth = ret_depth + # best_depth = ret_depth + # elif score == best_score and ret_depth < best_depth: + # best_depth = ret_depth return best_score,best_depth,results else: # if not is_maximizing: (it is the adversary's turn) - valid_steps = self.get_valid_steps(chess_board,my_pos,adv_pos,max_step) - # filter stupid steps - stupid_steps_0 = set(filter(lambda step: self.check_stupid_step_0(chess_board,step),valid_steps)) # depth 0 stupid moves - stupid_steps_1 = set(filter(lambda step: self.check_stupid_step_1(chess_board,step,my_pos,max_step),valid_steps)) # depth 1 stupid moves - candidate_steps = valid_steps - (stupid_steps_0 | stupid_steps_1) + # valid_steps = self.get_valid_steps(chess_board,my_pos,adv_pos,max_step) + valid_steps = self.get_valid_steps(chess_board,adv_pos,my_pos,max_step) #CHANGED get valid steps of the opposing agent + + # filter depth 0 and 1 stupid moves + stupid_steps = set(filter(lambda step: self.check_stupid_step(chess_board,step,my_pos,max_step),valid_steps)) + candidate_steps = valid_steps - stupid_steps # (stupid_steps_0 | stupid_steps_1) + for step in candidate_steps: # move to each square... + if (time.time() - self.timer) > self.time_limit: + break + r,c,dir = (step[0],step[1],step[2]) self.set_barrier(chess_board, r, c, dir) # and unset_barrier at the same position later - # Add this move to the move history - # previous_moves.append(step) - - + # Run the minimax algo to check where to place our agent is the best score,ret_depth,ret_results = self.minimax(chess_board, my_pos, (r,c), max_step, board_size, True, depth-1) - # end_found, result, = self.minimax(...) - results = np.add(results,ret_results) + self.unset_barrier(chess_board, r, c, dir) - + + results = np.add(results,ret_results) + best_depth = ret_depth # ADDED + if score == 0: best_score = 0 - best_depth = ret_depth + # best_depth = ret_depth break - elif score > best_score: # technically means the opposing agent prefers a draw even tho we prefer a win + # elif score > best_score: # technically means the opposing agent prefers a draw even tho we prefer a win + elif score < best_score: #CHANGED best_score = score - best_depth = ret_depth - elif score == best_score and ret_depth < best_depth: - best_depth = ret_depth - return best_score,depth,results + # best_depth = ret_depth + # elif score == best_score and ret_depth < best_depth: + # best_depth = ret_depth + + return best_score,best_depth,results + - # M x M board. max_step = floor((M+1)/2) - # for each node in the search tree, it has children for every possible step number : - # some children after moving 1 step, moving 2 steps, ... 5 steps max. - # to reduce the branching factor of the search tree but still cover each possible distance, - # we can calculate only 2 positions for each number <= max_step, and 4 positions for the wall. def get_valid_steps(self, chess_board, my_pos, adv_pos, max_step): # returns set (can change to list if necessary) of valid steps from current position @@ -361,7 +367,7 @@ def get_empty_edges(self,chess_board,pos): empty_edges.append(dir) return empty_edges - def check_stupid_step_0(self,chess_board,step): # A 0-stupid_step makes us lose this turn + # def check_stupid_step_0(self,chess_board,step): """ This is not exhaustive of moves in which make us lose Checks for if move puts us in a 1x1 box, forcing us to lose @@ -372,22 +378,29 @@ def check_stupid_step_0(self,chess_board,step): # A 0-stupid_step makes us lose return True return False - def check_stupid_step_1(self, chess_board,step,adv_pos,max_step): # A 1-stupid step allows the opponent to beat us next turn + def check_stupid_step(self, chess_board,step,adv_pos,max_step): """ This is not exhaustive of moves in which we can lose next turn Checks for if move puts us in a box with 3 edges around it where the opponent can reach the empty edge in their next turn """ r,c,dir = step empty_edges = self.get_empty_edges(chess_board,(r,c)) + + # A 1-stupid step allows the opponent to beat us next turn if (len(empty_edges) == 2): # might be stupid empty_edges.remove(dir) + + # A 0-stupid_step makes us lose this turn + # filter the steps that put agent in a 3-wall position and within the reach of the opponent + if (len(empty_edges) == 1): empty_dir = empty_edges[0] - move = self.moves[empty_dir] + move = self.moves[empty_dir] # find the position on the other side of wall win_pos = (r+move[0],c+move[1]) if self.check_valid_step(chess_board,adv_pos,win_pos,self.opposites[empty_dir],(r,c),max_step): return True return False + def check_valid_step(self, chess_board, my_start_pos, my_end_pos, dir, adv_pos, max_step): """ Check if the step the agent takes is valid (reachable and within max steps). diff --git a/agents/test_agent.py b/agents/test_agent.py index ee4024f..b06108e 100644 --- a/agents/test_agent.py +++ b/agents/test_agent.py @@ -281,6 +281,32 @@ def check_stupid_2(chess_board,step,adv_pos,max_step): return filtered_steps + def check_stupid_step_0(self,chess_board,step): # A 0-stupid_step makes us lose this turn + """ + This is not exhaustive of moves in which make us lose + Checks for if move puts us in a 1x1 box, forcing us to lose + """ + r,c = (step[0],step[1]) + empty_edges = self.get_empty_edges(chess_board,(r,c)) + if (len(empty_edges) == 1): + return True + return False + + def check_stupid_step_1(self, chess_board,step,adv_pos,max_step): # A 1-stupid step allows the opponent to beat us next turn + """ + This is not exhaustive of moves in which we can lose next turn + Checks for if move puts us in a box with 3 edges around it where the opponent can reach the empty edge in their next turn + """ + r,c,dir = step + empty_edges = self.get_empty_edges(chess_board,(r,c)) + if (len(empty_edges) == 2): # might be stupid + empty_edges.remove(dir) + empty_dir = empty_edges[0] + move = self.moves[empty_dir] + win_pos = (r+move[0],c+move[1]) + if self.check_valid_step(chess_board,adv_pos,win_pos,self.opposites[empty_dir],(r,c),max_step): + return True + return False diff --git a/simulator.py b/simulator.py index 6302f91..2d4e9c2 100644 --- a/simulator.py +++ b/simulator.py @@ -32,7 +32,7 @@ def get_args(): parser.add_argument("--display_save", action="store_true", default=False) parser.add_argument("--display_save_path", type=str, default="plots/") parser.add_argument("--autoplay", action="store_true", default=False) - parser.add_argument("--autoplay_runs", type=int, default=1000) + parser.add_argument("--autoplay_runs", type=int, default=100) args = parser.parse_args() return args From 8539b1bfc63cc946d0f970d48e6b18dd08b9bd5b Mon Sep 17 00:00:00 2001 From: cWetaski Date: Fri, 8 Apr 2022 15:03:27 -0400 Subject: [PATCH 19/25] added many print statements for debugging performance (these should be removed before we submit). Revert some changes to move selection hierarchy --- agents/student_agent.py | 269 ++++++++++++++++++++++------------------ play.py | 8 +- 2 files changed, 152 insertions(+), 125 deletions(-) diff --git a/agents/student_agent.py b/agents/student_agent.py index 8200cc6..39da9a2 100644 --- a/agents/student_agent.py +++ b/agents/student_agent.py @@ -30,96 +30,11 @@ def __init__(self): self.first_turn = True self.timer = 0 self.time_limit = 28 - self.endgame_timer = 0 #??? + self.search_count = 0 # count number of times search_valid_pos is run per turn + self.step_count = 0 # count number of steps considered per turn + self.step_get_time = 0 # track total number of time spent getting valid steps + self.check_end_time = 0 # track time spent in checking endgames - # functions set_barrier(), check_valid_step(), check_endgame() below copied from world.py: - def set_barrier(self, chess_board, r, c, dir): # adapted from world.py function of same name - # Set the barrier to True - chess_board[r, c, dir] = True - # Set the opposite barrier to True - move = self.moves[dir] - chess_board[r + move[0], c + move[1], self.opposites[dir]] = True - - def unset_barrier(self, chess_board, r, c, dir): - # Set the barrier to False - chess_board[r, c, dir] = False - # Set the opposite barrier to False - move = self.moves[dir] - chess_board[r + move[0], c + move[1], self.opposites[dir]] = False - - def check_endgame(self, chess_board, my_pos, adv_pos,board_size): - """ - Check if the game ends and compute the current score of the agents. - - Returns - ------- - is_endgame : bool - Whether the game ends. - player_1_score : int - The score of player 1. - player_2_score : int - The score of player 2. - """ - start = time.time() - - # Union-Find - father = dict() - for r in range(board_size): - for c in range(board_size): - father[(r, c)] = (r, c) - - def find(pos): - if father[pos] != pos: - father[pos] = find(father[pos]) - return father[pos] - - def union(pos1, pos2): - father[pos1] = pos2 - - for r in range(board_size): - for c in range(board_size): - for dir, move in enumerate( - self.moves[1:3] - ): # Only check down and right - if chess_board[r, c, dir + 1]: - continue - pos_a = find((r, c)) - pos_b = find((r + move[0], c + move[1])) - if pos_a != pos_b: - union(pos_a, pos_b) - - for r in range(board_size): - for c in range(board_size): - find((r, c)) - - p0_r = find(tuple(my_pos)) - p1_r = find(tuple(adv_pos)) - p0_score = list(father.values()).count(p0_r) - p1_score = list(father.values()).count(p1_r) - if p0_r == p1_r: - return False, p0_score, p1_score - ''' - player_win = None - win_blocks = -1 - if p0_score > p1_score: - player_win = 0 - win_blocks = p0_score - elif p0_score < p1_score: - player_win = 1 - win_blocks = p1_score - else: - player_win = -1 # Tie - if player_win >= 0: - logging.info( - f"Game ends! Player {self.player_names[player_win]} wins having control over {win_blocks} blocks!" - ) - else: - logging.info("Game ends! It is a Tie!") - ''' - end = time.time() - self.endgame_timer = self.endgame_timer + (end - start) - return True, p0_score, p1_score - # THE ACTAUL IMPLEMENTATION... def step(self, chess_board, my_pos, adv_pos, max_step): """ @@ -136,13 +51,15 @@ def step(self, chess_board, my_pos, adv_pos, max_step): Please check the sample implementation in agents/random_agent.py or agents/human_agent.py for more details. """ - + self.search_count = 0 + self.step_count = 0 + self.step_get_time = 0 + self.check_end_time = 0 + board_size = chess_board.shape[1] - depth_limit = 4 - if board_size > 7: + depth_limit = 3 + if board_size > 8: depth_limit = 2 - self.endgame_timer = 0 - self.check_step_timer = 0 # not used self.timer = time.time() # record the current time at the start of each step best_score = -1 @@ -160,9 +77,16 @@ def step(self, chess_board, my_pos, adv_pos, max_step): if len(candidate_steps) == 0: if(len(stupid_steps > 0)): best_move = stupid_steps.pop() - # else: - # best_move = stupid_steps_0.pop() + + count = 0 + reduce_depth = True + + if(len(candidate_steps) < 20): # if there are few moves available, increase depth limit + depth_limit = depth_limit + 1 + for step in candidate_steps: + count += 1 + self.step_count += 1 if (time.time() - self.timer) > self.time_limit: # print('time limit exceeded') break @@ -172,31 +96,37 @@ def step(self, chess_board, my_pos, adv_pos, max_step): # Run the minimax algo to check where to place our agent is the best # this is the minimizing node - score, ret_depth, results = self.minimax(chess_board, (r,c), adv_pos, max_step, board_size, False, depth_limit) + score, ret_depth, results = self.minimax(chess_board, (r,c), adv_pos, max_step, board_size, False,1, depth_limit) self.unset_barrier(chess_board, r, c, dir) weighted_score = (results[0]-results[1])/(results[2]) - best_depth = ret_depth # ADDED - + # explanation: the series of elif statements are essentiall a decision hierarchy of deciding the best move where score -> depth -> weighted_score is the priority of move decision. + # therefore, whenever we are assigning the best move in the hierarchy, we need to also assign the best_depth and the best_weighted_score at that location so that the information + # of the current best_move is stored. if score == 1: best_move = ((r,c),dir) + print('found winner') break elif score > best_score: best_move = ((r,c),dir) - # best_depth = ret_depth # ADDED ABOVE - # best_weighted_score = weighted_score # don't need it here + best_depth = ret_depth + best_weighted_score = weighted_score # don't need it here # yes we do! # losing or drawing closer to 0 depth (bottom of search tree) is preferred elif score == best_score and ret_depth < best_depth: best_move = ((r,c),dir) - # best_depth = ret_depth - # best_weighted_score #??? + best_depth = ret_depth + best_weighted_score = weighted_score # the weighted score of the best move, not the best_weighted score of any move # a move which has a higher weighted score is better, all other things equal elif score == best_score and ret_depth == best_depth and weighted_score > best_weighted_score: best_move = ((r,c),dir) - # best_depth = ret_depth + best_depth = ret_depth best_weighted_score = weighted_score + + if best_score == 0.75 and reduce_depth: # do a deeper search to find a candidate step that doesn't lose or draw, then do shallower search to allow us to check every possibility for a quick win + reduce_depth = False + depth_limit = depth_limit - 1 if self.first_turn == True: self.first_turn = False @@ -204,9 +134,16 @@ def step(self, chess_board, my_pos, adv_pos, max_step): if (time.time() - self.timer) > self.time_limit: print(time.time() - self.timer) + + print(count) + print(len(candidate_steps)) + print(self.search_count) + print(self.step_count) + print(self.step_get_time) + print(self.check_end_time) return best_move - def minimax(self, chess_board, my_pos, adv_pos, max_step, board_size, is_maximizing, depth): + def minimax(self, chess_board, my_pos, adv_pos, max_step, board_size, is_maximizing, depth,depth_limit): """ is_maximizing is a bool indicating whether we are at maximizing or minimizing step depth is the depth of search @@ -221,6 +158,7 @@ def minimax(self, chess_board, my_pos, adv_pos, max_step, board_size, is_maximiz results: [int,int,int] array of results discovered (wins-draws-losses) """ + self.step_count += 1 # Check base cases: is_end, s1, s2 = self.check_endgame(chess_board, my_pos, adv_pos,board_size) @@ -237,7 +175,7 @@ def minimax(self, chess_board, my_pos, adv_pos, max_step, board_size, is_maximiz else: return 0.5,depth,results - if depth == 0: # if depth limit reached (height limit actually) + if depth == depth_limit: # if depth limit reached (height limit actually) return 0.75,depth,results best_score = 0 @@ -260,22 +198,22 @@ def minimax(self, chess_board, my_pos, adv_pos, max_step, board_size, is_maximiz self.set_barrier(chess_board, r, c, dir) # and unset_barrier at the same position later # Run the minimax algo to check where to place our agent is the best - score,ret_depth,ret_results = self.minimax(chess_board, (r,c), adv_pos, max_step, board_size, False, depth-1) + score,ret_depth,ret_results = self.minimax(chess_board, (r,c), adv_pos, max_step, board_size, False, depth+1,depth_limit) self.unset_barrier(chess_board, r, c, dir) results = np.add(results,ret_results) # add results which were found - best_depth = ret_depth #ADDED + if score == 1: best_score = 1 - # best_depth = ret_depth + best_depth = ret_depth break elif score > best_score: best_score = score - # best_depth = ret_depth - # elif score == best_score and ret_depth < best_depth: - # best_depth = ret_depth + best_depth = ret_depth #ADDED, Best_depth must be assigned within the elif statements + elif score == best_score and ret_depth > best_depth: + best_depth = ret_depth return best_score,best_depth,results @@ -296,30 +234,99 @@ def minimax(self, chess_board, my_pos, adv_pos, max_step, board_size, is_maximiz self.set_barrier(chess_board, r, c, dir) # and unset_barrier at the same position later # Run the minimax algo to check where to place our agent is the best - score,ret_depth,ret_results = self.minimax(chess_board, my_pos, (r,c), max_step, board_size, True, depth-1) + score,ret_depth,ret_results = self.minimax(chess_board, my_pos, (r,c), max_step, board_size, True, depth+1,depth_limit) self.unset_barrier(chess_board, r, c, dir) results = np.add(results,ret_results) - best_depth = ret_depth # ADDED if score == 0: best_score = 0 - # best_depth = ret_depth + best_depth = ret_depth break - # elif score > best_score: # technically means the opposing agent prefers a draw even tho we prefer a win - elif score < best_score: #CHANGED + elif score < best_score: #CHANGED , Best_depth must be assigned within the elif statements best_score = score - # best_depth = ret_depth - # elif score == best_score and ret_depth < best_depth: - # best_depth = ret_depth + best_depth = ret_depth + elif score == best_score and ret_depth > best_depth: + best_depth = ret_depth return best_score,best_depth,results + def check_endgame(self, chess_board, my_pos, adv_pos,board_size): + """ + Check if the game ends and compute the current score of the agents. + Returns + ------- + is_endgame : bool + Whether the game ends. + player_1_score : int + The score of player 1. + player_2_score : int + The score of player 2. + """ + start = time.time() + # Union-Find + father = dict() + for r in range(board_size): + for c in range(board_size): + father[(r, c)] = (r, c) - def get_valid_steps(self, chess_board, my_pos, adv_pos, max_step): # returns set (can change to list if necessary) of valid steps from current position + def find(pos): + if father[pos] != pos: + father[pos] = find(father[pos]) + return father[pos] + + def union(pos1, pos2): + father[pos1] = pos2 + + for r in range(board_size): + for c in range(board_size): + for dir, move in enumerate( + self.moves[1:3] + ): # Only check down and right + if chess_board[r, c, dir + 1]: + continue + pos_a = find((r, c)) + pos_b = find((r + move[0], c + move[1])) + if pos_a != pos_b: + union(pos_a, pos_b) + + for r in range(board_size): + for c in range(board_size): + find((r, c)) + p0_r = find(tuple(my_pos)) + p1_r = find(tuple(adv_pos)) + p0_score = list(father.values()).count(p0_r) + p1_score = list(father.values()).count(p1_r) + + self.check_end_time += (time.time() - start) + if p0_r == p1_r: + return False, p0_score, p1_score + ''' + player_win = None + win_blocks = -1 + if p0_score > p1_score: + player_win = 0 + win_blocks = p0_score + elif p0_score < p1_score: + player_win = 1 + win_blocks = p1_score + else: + player_win = -1 # Tie + if player_win >= 0: + logging.info( + f"Game ends! Player {self.player_names[player_win]} wins having control over {win_blocks} blocks!" + ) + else: + logging.info("Game ends! It is a Tie!") + ''' + return True, p0_score, p1_score + + + def get_valid_steps(self, chess_board, my_pos, adv_pos, max_step): # returns set (can change to list if necessary) of valid steps from current position + start = time.time() end_posits = [my_pos] valid_steps = set() end_posits = self.search_valid_pos(chess_board,my_pos,adv_pos,max_step) @@ -327,6 +334,7 @@ def get_valid_steps(self, chess_board, my_pos, adv_pos, max_step): # returns set empty_edges = self.get_empty_edges(chess_board,end_pos) for dir in empty_edges: valid_steps.add((end_pos[0],end_pos[1],dir)) + self.step_get_time += (time.time()-start) return valid_steps def search_valid_pos(self, chess_board, my_start_pos, adv_pos, max_step): @@ -340,6 +348,7 @@ def search_valid_pos(self, chess_board, my_start_pos, adv_pos, max_step): The end position of the agent. """ # BFS + self.search_count = self.search_count + 1 state_queue = [(my_start_pos, 0)] visited = {tuple(my_start_pos)} cur_step = 0 @@ -448,5 +457,23 @@ def check_valid_step(self, chess_board, my_start_pos, my_end_pos, dir, adv_pos, state_queue.append((next_pos, cur_step + 1)) return is_reached + # functions set_barrier(), check_valid_step(), check_endgame() below copied from world.py: + def set_barrier(self, chess_board, r, c, dir): # adapted from world.py function of same name + # Set the barrier to True + chess_board[r, c, dir] = True + # Set the opposite barrier to True + move = self.moves[dir] + chess_board[r + move[0], c + move[1], self.opposites[dir]] = True + + def unset_barrier(self, chess_board, r, c, dir): + # Set the barrier to False + chess_board[r, c, dir] = False + # Set the opposite barrier to False + move = self.moves[dir] + chess_board[r + move[0], c + move[1], self.opposites[dir]] = False + + + + \ No newline at end of file diff --git a/play.py b/play.py index 3fa4027..1e13c6e 100644 --- a/play.py +++ b/play.py @@ -7,12 +7,12 @@ args.board_size = 5 args.autoplay = True args.autoplay_runs = 10 -args.board_size_min = 9 -args.board_size_max = 10 +args.board_size_min = 6 +args.board_size_max = 7 args.board_size = 6 s1 = simulator.Simulator(args) -result = s1.autoplay() -#result = s1.run() +#result = s1.autoplay() +result = s1.run() From f46ac2c64c73fec5ec943d4c2740274e87fe2d16 Mon Sep 17 00:00:00 2001 From: cWetaski Date: Fri, 8 Apr 2022 15:59:00 -0400 Subject: [PATCH 20/25] iterative deepening, removed print statements --- agents/student_agent.py | 289 ++++++++++++++-------------- agents/student_agent_template.py | 40 ---- agents/test_agent.py | 314 ------------------------------- play.py | 6 +- 4 files changed, 149 insertions(+), 500 deletions(-) delete mode 100644 agents/student_agent_template.py delete mode 100644 agents/test_agent.py diff --git a/agents/student_agent.py b/agents/student_agent.py index 39da9a2..4bec7e0 100644 --- a/agents/student_agent.py +++ b/agents/student_agent.py @@ -30,10 +30,10 @@ def __init__(self): self.first_turn = True self.timer = 0 self.time_limit = 28 - self.search_count = 0 # count number of times search_valid_pos is run per turn - self.step_count = 0 # count number of steps considered per turn - self.step_get_time = 0 # track total number of time spent getting valid steps - self.check_end_time = 0 # track time spent in checking endgames + # self.search_count = 0 # count number of times search_valid_pos is run per turn + # self.step_count = 0 # count number of steps considered per turn + # self.step_get_time = 0 # track total number of time spent getting valid steps + # self.check_end_time = 0 # track time spent in checking endgames # THE ACTAUL IMPLEMENTATION... def step(self, chess_board, my_pos, adv_pos, max_step): @@ -57,13 +57,10 @@ def step(self, chess_board, my_pos, adv_pos, max_step): self.check_end_time = 0 board_size = chess_board.shape[1] - depth_limit = 3 - if board_size > 8: - depth_limit = 2 self.timer = time.time() # record the current time at the start of each step best_score = -1 - best_depth = depth_limit + best_depth = 0 # Heuristics to choose the move with a higher percentage of winning rate weighted_score = 0 @@ -80,68 +77,75 @@ def step(self, chess_board, my_pos, adv_pos, max_step): count = 0 reduce_depth = True + + for n in range(1,3): # iterative deepening (two times) + # depth limit logic + if n == 1: + # check for winning move even though it would be more efficient to store this, it is only ~40-100 checks which is quite small relative the total + # number of checks that we do (~14000 before hitting the time limit) + # would be more efficient to implement a breadth-first search, but I don't have time to do that + depth_limit = 0 + else: + depth_limit = 1 # go to depth 1 + if board_size < 9: # if smaller board, go deeper + depth_limit = depth_limit + 1 + if self.first_turn: # if first turn, go deeper + depth_limit = depth_limit + 1 + if len(candidate_steps) < 20: # if few options available, go deeper + depth_limit = depth_limit + 1 + for step in candidate_steps: + count += 1 + self.step_count += 1 + # a new move of step-size steps has been generated here--- + r,c,dir = (step[0],step[1],step[2]) + self.set_barrier(chess_board,r,c,dir) + + # Run the minimax algo to check where to place our agent is the best + # this is the minimizing node + score, ret_depth, results = self.minimax(chess_board, (r,c), adv_pos, max_step, board_size, False,0, depth_limit) + + self.unset_barrier(chess_board, r, c, dir) - if(len(candidate_steps) < 20): # if there are few moves available, increase depth limit - depth_limit = depth_limit + 1 + weighted_score = (results[0]-results[1])/(results[2]) - for step in candidate_steps: - count += 1 - self.step_count += 1 - if (time.time() - self.timer) > self.time_limit: - # print('time limit exceeded') - break - # a new move of step-size steps has been generated here--- - r,c,dir = (step[0],step[1],step[2]) - self.set_barrier(chess_board,r,c,dir) - - # Run the minimax algo to check where to place our agent is the best - # this is the minimizing node - score, ret_depth, results = self.minimax(chess_board, (r,c), adv_pos, max_step, board_size, False,1, depth_limit) - - self.unset_barrier(chess_board, r, c, dir) + # explanation: the series of elif statements are essentiall a decision hierarchy of deciding the best move where score -> depth -> weighted_score is the priority of move decision. + # therefore, whenever we are assigning the best move in the hierarchy, we need to also assign the best_depth and the best_weighted_score at that location so that the information + # of the current best_move is stored. + if score == 1: + best_move = ((r,c),dir) + break + elif score > best_score: + best_move = ((r,c),dir) + best_depth = ret_depth + best_weighted_score = weighted_score # don't need it here # yes we do! + # losing or drawing closer to 0 depth (bottom of search tree) is preferred + elif score == best_score and ret_depth > best_depth: + best_move = ((r,c),dir) + best_depth = ret_depth + best_weighted_score = weighted_score # the weighted score of the best move, not the best_weighted score of any move + # a move which has a higher weighted score is better, all other things equal + elif score == best_score and ret_depth == best_depth and weighted_score > best_weighted_score: + best_move = ((r,c),dir) + best_depth = ret_depth + best_weighted_score = weighted_score + + if best_score == 0.75 and reduce_depth: # if depth limit has been reached for an indeterminate step, do shallower search to allow us to check every possibility for a quick win + reduce_depth = False + depth_limit = depth_limit - 1 - weighted_score = (results[0]-results[1])/(results[2]) + if (time.time() - self.timer) > self.time_limit: + # print('time limit exceeded') + break - # explanation: the series of elif statements are essentiall a decision hierarchy of deciding the best move where score -> depth -> weighted_score is the priority of move decision. - # therefore, whenever we are assigning the best move in the hierarchy, we need to also assign the best_depth and the best_weighted_score at that location so that the information - # of the current best_move is stored. - if score == 1: - best_move = ((r,c),dir) - print('found winner') - break - elif score > best_score: - best_move = ((r,c),dir) - best_depth = ret_depth - best_weighted_score = weighted_score # don't need it here # yes we do! - # losing or drawing closer to 0 depth (bottom of search tree) is preferred - elif score == best_score and ret_depth < best_depth: - best_move = ((r,c),dir) - best_depth = ret_depth - best_weighted_score = weighted_score # the weighted score of the best move, not the best_weighted score of any move - # a move which has a higher weighted score is better, all other things equal - elif score == best_score and ret_depth == best_depth and weighted_score > best_weighted_score: - best_move = ((r,c),dir) - best_depth = ret_depth - best_weighted_score = weighted_score - - if best_score == 0.75 and reduce_depth: # do a deeper search to find a candidate step that doesn't lose or draw, then do shallower search to allow us to check every possibility for a quick win - reduce_depth = False - depth_limit = depth_limit - 1 + # if (time.time() - self.timer) > self.time_limit: + # print(time.time() - self.timer) if self.first_turn == True: self.first_turn = False self.time_limit = 1.9 - if (time.time() - self.timer) > self.time_limit: - print(time.time() - self.timer) - - print(count) - print(len(candidate_steps)) - print(self.search_count) - print(self.step_count) - print(self.step_get_time) - print(self.check_end_time) return best_move + def minimax(self, chess_board, my_pos, adv_pos, max_step, board_size, is_maximizing, depth,depth_limit): """ @@ -158,7 +162,7 @@ def minimax(self, chess_board, my_pos, adv_pos, max_step, board_size, is_maximiz results: [int,int,int] array of results discovered (wins-draws-losses) """ - self.step_count += 1 + # self.step_count += 1 # Check base cases: is_end, s1, s2 = self.check_endgame(chess_board, my_pos, adv_pos,board_size) @@ -191,12 +195,10 @@ def minimax(self, chess_board, my_pos, adv_pos, max_step, board_size, is_maximiz candidate_steps = valid_steps - stupid_steps #(stupid_steps_0 | stupid_steps_1) for step in candidate_steps: # move to each square... - if (time.time() - self.timer) > self.time_limit: - break r,c,dir = (step[0],step[1],step[2]) self.set_barrier(chess_board, r, c, dir) # and unset_barrier at the same position later - + # Run the minimax algo to check where to place our agent is the best score,ret_depth,ret_results = self.minimax(chess_board, (r,c), adv_pos, max_step, board_size, False, depth+1,depth_limit) @@ -214,6 +216,9 @@ def minimax(self, chess_board, my_pos, adv_pos, max_step, board_size, is_maximiz best_depth = ret_depth #ADDED, Best_depth must be assigned within the elif statements elif score == best_score and ret_depth > best_depth: best_depth = ret_depth + + if (time.time() - self.timer) > self.time_limit: + break return best_score,best_depth,results @@ -227,8 +232,6 @@ def minimax(self, chess_board, my_pos, adv_pos, max_step, board_size, is_maximiz candidate_steps = valid_steps - stupid_steps # (stupid_steps_0 | stupid_steps_1) for step in candidate_steps: # move to each square... - if (time.time() - self.timer) > self.time_limit: - break r,c,dir = (step[0],step[1],step[2]) self.set_barrier(chess_board, r, c, dir) # and unset_barrier at the same position later @@ -250,83 +253,14 @@ def minimax(self, chess_board, my_pos, adv_pos, max_step, board_size, is_maximiz elif score == best_score and ret_depth > best_depth: best_depth = ret_depth - return best_score,best_depth,results - - def check_endgame(self, chess_board, my_pos, adv_pos,board_size): - """ - Check if the game ends and compute the current score of the agents. - - Returns - ------- - is_endgame : bool - Whether the game ends. - player_1_score : int - The score of player 1. - player_2_score : int - The score of player 2. - """ - start = time.time() - # Union-Find - father = dict() - for r in range(board_size): - for c in range(board_size): - father[(r, c)] = (r, c) - - def find(pos): - if father[pos] != pos: - father[pos] = find(father[pos]) - return father[pos] - - def union(pos1, pos2): - father[pos1] = pos2 - - for r in range(board_size): - for c in range(board_size): - for dir, move in enumerate( - self.moves[1:3] - ): # Only check down and right - if chess_board[r, c, dir + 1]: - continue - pos_a = find((r, c)) - pos_b = find((r + move[0], c + move[1])) - if pos_a != pos_b: - union(pos_a, pos_b) + if (time.time() - self.timer) > self.time_limit: + break - for r in range(board_size): - for c in range(board_size): - find((r, c)) - - p0_r = find(tuple(my_pos)) - p1_r = find(tuple(adv_pos)) - p0_score = list(father.values()).count(p0_r) - p1_score = list(father.values()).count(p1_r) - - self.check_end_time += (time.time() - start) - if p0_r == p1_r: - return False, p0_score, p1_score - ''' - player_win = None - win_blocks = -1 - if p0_score > p1_score: - player_win = 0 - win_blocks = p0_score - elif p0_score < p1_score: - player_win = 1 - win_blocks = p1_score - else: - player_win = -1 # Tie - if player_win >= 0: - logging.info( - f"Game ends! Player {self.player_names[player_win]} wins having control over {win_blocks} blocks!" - ) - else: - logging.info("Game ends! It is a Tie!") - ''' - return True, p0_score, p1_score + return best_score,best_depth,results def get_valid_steps(self, chess_board, my_pos, adv_pos, max_step): # returns set (can change to list if necessary) of valid steps from current position - start = time.time() + # start = time.time() end_posits = [my_pos] valid_steps = set() end_posits = self.search_valid_pos(chess_board,my_pos,adv_pos,max_step) @@ -334,7 +268,7 @@ def get_valid_steps(self, chess_board, my_pos, adv_pos, max_step): # returns set empty_edges = self.get_empty_edges(chess_board,end_pos) for dir in empty_edges: valid_steps.add((end_pos[0],end_pos[1],dir)) - self.step_get_time += (time.time()-start) + # self.step_get_time += (time.time()-start) return valid_steps def search_valid_pos(self, chess_board, my_start_pos, adv_pos, max_step): @@ -348,7 +282,7 @@ def search_valid_pos(self, chess_board, my_start_pos, adv_pos, max_step): The end position of the agent. """ # BFS - self.search_count = self.search_count + 1 + # self.search_count = self.search_count + 1 state_queue = [(my_start_pos, 0)] visited = {tuple(my_start_pos)} cur_step = 0 @@ -409,7 +343,6 @@ def check_stupid_step(self, chess_board,step,adv_pos,max_step): return True return False - def check_valid_step(self, chess_board, my_start_pos, my_end_pos, dir, adv_pos, max_step): """ Check if the step the agent takes is valid (reachable and within max steps). @@ -472,7 +405,77 @@ def unset_barrier(self, chess_board, r, c, dir): move = self.moves[dir] chess_board[r + move[0], c + move[1], self.opposites[dir]] = False - + def check_endgame(self, chess_board, my_pos, adv_pos,board_size): + """ + Check if the game ends and compute the current score of the agents. + + Returns + ------- + is_endgame : bool + Whether the game ends. + player_1_score : int + The score of player 1. + player_2_score : int + The score of player 2. + """ + # start = time.time() + # Union-Find + father = dict() + for r in range(board_size): + for c in range(board_size): + father[(r, c)] = (r, c) + + def find(pos): + if father[pos] != pos: + father[pos] = find(father[pos]) + return father[pos] + + def union(pos1, pos2): + father[pos1] = pos2 + + for r in range(board_size): + for c in range(board_size): + for dir, move in enumerate( + self.moves[1:3] + ): # Only check down and right + if chess_board[r, c, dir + 1]: + continue + pos_a = find((r, c)) + pos_b = find((r + move[0], c + move[1])) + if pos_a != pos_b: + union(pos_a, pos_b) + + for r in range(board_size): + for c in range(board_size): + find((r, c)) + + p0_r = find(tuple(my_pos)) + p1_r = find(tuple(adv_pos)) + p0_score = list(father.values()).count(p0_r) + p1_score = list(father.values()).count(p1_r) + + # self.check_end_time += (time.time() - start) + if p0_r == p1_r: + return False, p0_score, p1_score + ''' + player_win = None + win_blocks = -1 + if p0_score > p1_score: + player_win = 0 + win_blocks = p0_score + elif p0_score < p1_score: + player_win = 1 + win_blocks = p1_score + else: + player_win = -1 # Tie + if player_win >= 0: + logging.info( + f"Game ends! Player {self.player_names[player_win]} wins having control over {win_blocks} blocks!" + ) + else: + logging.info("Game ends! It is a Tie!") + ''' + return True, p0_score, p1_score diff --git a/agents/student_agent_template.py b/agents/student_agent_template.py deleted file mode 100644 index 5780ea2..0000000 --- a/agents/student_agent_template.py +++ /dev/null @@ -1,40 +0,0 @@ -# Student agent: Add your own agent here -from agents.agent import Agent -from store import register_agent -import sys - - -@register_agent("student_agent") -class StudentAgent(Agent): - """ - A dummy class for your implementation. Feel free to use this class to - add any helper functionalities needed for your agent. - """ - - def __init__(self): - super(StudentAgent, self).__init__() - self.name = "StudentAgent" - self.dir_map = { - "u": 0, - "r": 1, - "d": 2, - "l": 3, - } - - def step(self, chess_board, my_pos, adv_pos, max_step): - """ - Implement the step function of your agent here. - You can use the following variables to access the chess board: - - chess_board: a numpy array of shape (x_max, y_max, 4) 3-dimentional - - my_pos: a tuple of (x, y) - - adv_pos: a tuple of (x, y) - - max_step: an integer - - You should return a tuple of ((x, y), dir), - where (x, y) is the next position of your agent and dir is the direction of the wall - you want to put on. - - Please check the sample implementation in agents/random_agent.py or agents/human_agent.py for more details. - """ - # dummy return - return my_pos, self.dir_map["u"] diff --git a/agents/test_agent.py b/agents/test_agent.py deleted file mode 100644 index b06108e..0000000 --- a/agents/test_agent.py +++ /dev/null @@ -1,314 +0,0 @@ - -# Student agent: Add your own agent here -from operator import truediv -from agents.agent import Agent -from store import register_agent -import sys -import numpy as np -from copy import deepcopy - -@register_agent("test_agent") -class TestAgent(Agent): - - def __init__(self): - super(TestAgent, self).__init__() - self.name = "TestAgent" - self.dir_map = { - "u": 0, - "r": 1, - "d": 2, - "l": 3, - } - self.moves = ((-1, 0), (0, 1), (1, 0), (0, -1)) # moves as defined in world.py (useful for reusing world.py code) - self.opposites = {0: 2, 1: 3, 2: 0, 3: 1} # opposite directions as defined in world.py - - def step(self, chess_board, my_pos, adv_pos, max_step): - valid_steps = self.get_valid_steps(chess_board, my_pos, adv_pos, max_step) - terminal_steps = set() - stupid_steps = set() - for step in valid_steps: - is_endgame, is_winner = self.sim_move(chess_board,step,adv_pos)[1:3] - num_edges = self.count_edges(chess_board,(step[0],step[1])) - if(is_endgame == True): - terminal_steps.add(tuple((step,is_winner))) - if(num_edges == 3): - stupid_steps.add(step) - print("Valid Steps:") - print(valid_steps) - print("Terminal Steps:") - print(terminal_steps) - print("Stupid Steps:") - print(stupid_steps) - not_stupid_steps = self.get_stupid_steps(chess_board,valid_steps,adv_pos,max_step) - print(not_stupid_steps) - - text = input("Your move (x,y,dir) or input q to quit: ") - - while len(text.split(",")) != 3 and "q" not in text.lower(): - print("Wrong Input Format!") - text = input("Your move (x,y,dir) or input q to quit: ") - if "q" in text.lower(): - print("Game ended by user!") - sys.exit(0) - x, y, dir = text.split(",") - x, y, dir = x.strip(), y.strip(), dir.strip() - x, y = int(x), int(y) - while not self.check_valid_input( - x, y, dir, chess_board.shape[0], chess_board.shape[1] - ): - print( - "Invalid Move! (x, y) should be within the board and dir should be one of u,r,d,l." - ) - text = input("Your move (x,y,dir) or input q to quit: ") - while len(text.split(",")) != 3 and "q" not in text.lower(): - print("Wrong Input Format!") - text = input("Your move (x,y,dir) or input q to quit: ") - if "q" in text.lower(): - print("Game ended by user!") - sys.exit(0) - x, y, dir = text.split(",") - x, y, dir = x.strip(), y.strip(), dir.strip() - x, y = int(x), int(y) - my_pos = (x, y) - return my_pos, self.dir_map[dir] - - def check_valid_input(self, x, y, dir, x_max, y_max): - return 0 <= x < x_max and 0 <= y < y_max and dir in self.dir_map - - def get_valid_steps(self, chess_board, my_pos, adv_pos, max_step): # returns set (can change to list if necessary) of valid steps from current position - board_size = chess_board.shape[0] - end_posits = [my_pos] - valid_steps = [] - for n in range(1,max_step+1): - for r_dist in range(0,n+1): - c_dist = n - r_dist - if r_dist == 0: - cur_steps = [(my_pos[0],my_pos[1] + c_dist),(my_pos[0],my_pos[1] - c_dist)] - elif c_dist == 0: - cur_steps = [(my_pos[0] + r_dist,my_pos[1]),(my_pos[0] - r_dist,my_pos[1])] - else: - cur_steps = [(my_pos[0] + r_dist,my_pos[1] + c_dist),( - my_pos[0] + r_dist,my_pos[1] - c_dist),( - my_pos[0] - r_dist,my_pos[1] + c_dist),( - my_pos[0] - r_dist,my_pos[1] - c_dist)] - end_posits.extend(cur_steps) - # filter steps which leave boundary or end in adversary's location - end_posits = set(filter(lambda end_pos: end_pos[0] < board_size and end_pos[1] < board_size and end_pos[0] >= 0 and end_pos[1] >= 0 and end_pos != adv_pos, end_posits)) - for end_pos in end_posits: - for dir in range(0,4): - if chess_board[end_pos[0],end_pos[1],dir]: - continue - valid_steps.append(tuple((end_pos[0],end_pos[1],dir))) - valid_steps = set(filter(lambda move: self.check_valid_step(np.asarray(my_pos),[move[0],move[1]],adv_pos, move[2], chess_board, max_step),valid_steps)) - return valid_steps - - def check_valid_step(self, start_pos, end_pos, adv_pos, barrier_dir, chess_board, max_step): # reused from world.py (modified to work in this context) - """ - Check if the step the agent takes is valid (reachable and within max steps). - - Parameters - ---------- - start_pos : tuple - The start position of the agent. - end_pos : np.ndarray - The end position of the agent. - barrier_dir : int - The direction of the barrier. - """ - # Endpoint already has barrier or is boarder - r, c = end_pos - if chess_board[r, c, barrier_dir]: - return False - if np.array_equal(start_pos, end_pos): - return True - - # BFS - state_queue = [(start_pos, 0)] - visited = {tuple(start_pos)} - is_reached = False - while state_queue and not is_reached: - cur_pos, cur_step = state_queue.pop(0) - r, c = cur_pos - if cur_step == max_step: - break - for dir, move in enumerate(self.moves): - if chess_board[r, c, dir]: - continue - next_pos = (cur_pos[0] + move[0],cur_pos[1]+move[1]) - if np.array_equal(next_pos, adv_pos) or tuple(next_pos) in visited: - continue - if np.array_equal(next_pos, end_pos): - is_reached = True - break - visited.add(tuple(next_pos)) - state_queue.append((next_pos, cur_step + 1)) - return is_reached - - def sim_move(self,chess_board,move,adv_pos): - """ - Assumption is that move the move is valid - Will add the move to a copy of the chess_board, and check if the move ends the game - - Returns - ------- - chess_board_copy : chess_board - Copy of chess_board with move applied - is_endgame : bool - Whether the game ends - is_winner : bool - (if is_endgame == True) Whether the player who made the move wins - (if is_endgame == False or if game is a tie) None - """ - - # create copy of the chess_board to view the new gamestate - chess_board_copy = deepcopy(chess_board) - - # apply move to copied chess_board - self.set_barrier(chess_board_copy,move[0],move[1],move[2]) - - # check stupid end - if self.count_edges(chess_board_copy,(move[0],move[1])) == 4: - return chess_board_copy, True, False - - # check if move ends game - cur_pos = (move[0],move[1]) - is_endgame, is_winner = self.check_endgame(chess_board_copy,cur_pos,adv_pos) - return chess_board_copy, is_endgame, is_winner - - def set_barrier(self, chess_board, r, c, dir): # adapted from world.py function of same name - # Set the barrier to True - chess_board[r, c, dir] = True - # Set the opposite barrier to True - move = self.moves[dir] - chess_board[r + move[0], c + move[1], self.opposites[dir]] = True - - def check_endgame(self,chess_board,p0_pos,p1_pos): # adapted from world.py function of same name - """ - Check if a game state is terminal and return who wins if so - Does not return score since it is not useful - - Returns - ------- - is_endgame : bool - Whether the game ends. - is_p0_winner : bool - Whether p0 wins (if is_endgame == false or if there is a tie, returns None) - """ - # Union-Find - father = dict() - for r in range(chess_board.shape[0]): - for c in range(chess_board.shape[0]): - father[(r, c)] = (r, c) - - def find(pos): - if father[pos] != pos: - father[pos] = find(father[pos]) - return father[pos] - - def union(pos1, pos2): - father[pos1] = pos2 - - for r in range(chess_board.shape[0]): - for c in range(chess_board.shape[0]): - for dir, move in enumerate( - self.moves[1:3] - ): # Only check down and right - if chess_board[r, c, dir + 1]: - continue - pos_a = find((r, c)) - pos_b = find((r + move[0], c + move[1])) - if pos_a != pos_b: - union(pos_a, pos_b) - - for r in range(chess_board.shape[0]): - for c in range(chess_board.shape[0]): - find((r, c)) - p0_r = find(tuple(p0_pos)) - p1_r = find(tuple(p1_pos)) - p0_score = list(father.values()).count(p0_r) - p1_score = list(father.values()).count(p1_r) - if p0_r == p1_r: - return False, None - is_p0_winner = None - if p0_score > p1_score: - is_p0_winner = True - elif p0_score < p1_score: - is_p0_winner = False - return True, is_p0_winner - - def count_edges(self,chess_board,pos): - count = 0 - for dir in range(0,4): - if chess_board[pos[0],pos[1],dir]: - count = count + 1 - return count - - def get_empty_edges(self,chess_board,pos): - empty_edges = [] - for dir in range(0,4): - if not (chess_board[pos[0],pos[1],dir]): - empty_edges.append(dir) - return empty_edges - - - - def get_stupid_steps(self,chess_board,steps,adv_pos,max_step): - """ - Returns stupid steps - In implementation it will make more sense for it to return steps which aren't stupid, - but for now this makes it easier to see if it is correctly identifying "stupid" steps - """ - filtered_steps = deepcopy(steps) - chess_board_copy = deepcopy(chess_board) - - # stupid condition 1: instantly makes us lose - # No need to query this, since it will already be queried when checking for terminal steps - - # stupid condition 2: put 3 walls around us when adversary is in range - def check_stupid_2(chess_board,step,adv_pos,max_step): - r,c,dir = step - empty_edges = self.get_empty_edges(chess_board,(r,c)) - if (self.count_edges(chess_board_copy,(r,c)) == 2): # might be stupid - empty_edges.remove(dir) - empty_dir = empty_edges[0] - move = self.moves[empty_dir] - win_pos = (r+move[0],c+move[1]) - if self.check_valid_step(tuple(adv_pos),win_pos,(r,c),self.opposites[empty_dir],chess_board,max_step): - return True - return False - - filtered_steps = set(filter(lambda step: check_stupid_2(chess_board,step,adv_pos,max_step),steps)) - return filtered_steps - - - def check_stupid_step_0(self,chess_board,step): # A 0-stupid_step makes us lose this turn - """ - This is not exhaustive of moves in which make us lose - Checks for if move puts us in a 1x1 box, forcing us to lose - """ - r,c = (step[0],step[1]) - empty_edges = self.get_empty_edges(chess_board,(r,c)) - if (len(empty_edges) == 1): - return True - return False - - def check_stupid_step_1(self, chess_board,step,adv_pos,max_step): # A 1-stupid step allows the opponent to beat us next turn - """ - This is not exhaustive of moves in which we can lose next turn - Checks for if move puts us in a box with 3 edges around it where the opponent can reach the empty edge in their next turn - """ - r,c,dir = step - empty_edges = self.get_empty_edges(chess_board,(r,c)) - if (len(empty_edges) == 2): # might be stupid - empty_edges.remove(dir) - empty_dir = empty_edges[0] - move = self.moves[empty_dir] - win_pos = (r+move[0],c+move[1]) - if self.check_valid_step(chess_board,adv_pos,win_pos,self.opposites[empty_dir],(r,c),max_step): - return True - return False - - - - - diff --git a/play.py b/play.py index 1e13c6e..1222bea 100644 --- a/play.py +++ b/play.py @@ -8,11 +8,11 @@ args.autoplay = True args.autoplay_runs = 10 args.board_size_min = 6 -args.board_size_max = 7 +args.board_size_max = 12 args.board_size = 6 s1 = simulator.Simulator(args) -#result = s1.autoplay() -result = s1.run() +result = s1.autoplay() +# result = s1.run() From f11534dba9396acc5b37c1eab7eea1f9b4ae1ed7 Mon Sep 17 00:00:00 2001 From: cWetaski Date: Fri, 8 Apr 2022 16:00:41 -0400 Subject: [PATCH 21/25] added charles author info --- authors.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/authors.yaml b/authors.yaml index 00c3b6d..6bddfe3 100644 --- a/authors.yaml +++ b/authors.yaml @@ -1,6 +1,6 @@ -- name: "Your full name as appears on your transcript" - mcgill_id: "Your mcgill id" - email: "email@mail.mcgill.ca" +- name: "Charles Wetaski" + mcgill_id: "260714346" + email: "charles.wetaski@mail.mcgill.ca" - name: "Your full name as appears on your transcript" mcgill_id: "Your mcgill id" From dd94bf08910b486324dc300bdd6985c892f9239e Mon Sep 17 00:00:00 2001 From: FFFlora0349 <59624826+FFFlora0349@users.noreply.github.com> Date: Fri, 8 Apr 2022 17:09:20 -0400 Subject: [PATCH 22/25] add flora name --- .DS_Store | Bin 0 -> 6148 bytes authors.yaml | 6 +- testing_log.txt | 974 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 977 insertions(+), 3 deletions(-) create mode 100644 .DS_Store create mode 100644 testing_log.txt diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..d9b35d2e1268712b32a45269a96faaebdbddafa7 GIT binary patch literal 6148 zcmeHKyG{c^3>?D=MWRVbxxc_4tfKG*`~Z(cG!ZE-^jGD(_%y~3B%*^VqKU?mcXoXq zuWpL-8Gx-mHuu01z=H0Gqc3yw=k7DRs)(aSjRVHb__}-9?{~B8-viFQ!86{l#XElR zSr7vQTZ}k-7xkb_3P=GdAO)m=6gWqL>ZXg^bBc&71*E`5D&XITMtAImQ(}BNxWouR z92gGcK4uAG^8m3IPKk`rEUCn#T8$W%bjDld^};DJ>9Dw&b?RoT3B}@e##^MrdZJb- zAO%hpxX$C!`~L&|kNN+Uq>~ho0vDx#4eQ7Cimz0?b@p=JYa9KE?m3@yH|~SNCE77D j+A$B_j_)EV^P2DZycbT1L1#SZMEwl7E;1?b-wJ#I&tw|A literal 0 HcmV?d00001 diff --git a/authors.yaml b/authors.yaml index 6bddfe3..c62c9dc 100644 --- a/authors.yaml +++ b/authors.yaml @@ -2,6 +2,6 @@ mcgill_id: "260714346" email: "charles.wetaski@mail.mcgill.ca" -- name: "Your full name as appears on your transcript" - mcgill_id: "Your mcgill id" - email: "email@mail.mcgill.ca" \ No newline at end of file +- name: "Flora Chai" + mcgill_id: "260888064" + email: "yuting.chai@mail.mcgill.ca" \ No newline at end of file diff --git a/testing_log.txt b/testing_log.txt new file mode 100644 index 0000000..35596ab --- /dev/null +++ b/testing_log.txt @@ -0,0 +1,974 @@ +Script started on Fri Apr 8 14:12:50 2022 + +The default interactive shell is now zsh. +To update your account to use zsh, please run `chsh -s /bin/zsh`. +For more details, please visit https://support.apple.com/kb/HT208050. +[?1034hbash-3.2$ exitpython simulator.py --player_1 random_agent --player_2 student_agent --autoplay + + 0%| | 0/100 [00:00 Date: Fri, 8 Apr 2022 23:24:41 -0400 Subject: [PATCH 23/25] deleted .test_agent --- agents/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/agents/__init__.py b/agents/__init__.py index bcdb77c..5ee095a 100644 --- a/agents/__init__.py +++ b/agents/__init__.py @@ -2,4 +2,3 @@ from .random_agent import RandomAgent from .human_agent import HumanAgent from .student_agent import StudentAgent -from .test_agent import TestAgent From 9bcf6d4919c0db986da22905cc3e052343236265 Mon Sep 17 00:00:00 2001 From: Charles Wetaski Date: Tue, 12 Apr 2022 19:18:50 -0400 Subject: [PATCH 24/25] Uploaded report --- report/report.pdf | Bin 0 -> 142568 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 report/report.pdf diff --git a/report/report.pdf b/report/report.pdf new file mode 100644 index 0000000000000000000000000000000000000000..0ca55f4d1544a7d4797fe8c89c43cb4af512338c GIT binary patch literal 142568 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� zrp={8r`X}jI+sOtn^7Gm_Et&(_p3wZ+k!;|oUs&RcP{=~A;UT6h%oS~2jQ?_{mgpf z{n+SO=`z!zAt<$Y_A$`5$FV{Bw+I5EQD7R2~G^(9kD)4qjQuHy4K z%`gOO-OZhv?gD{7fnBeehsZK@c92G1C~^*=9-{<(Yh>QukD2j06mKMF#0B1z`1B!< z#n#DXe3)#U<*nNkw~!{Y zu6m!8Uo%#AQEM?0b-Na`H$$t)j+d>4$nr5FA(KYe5pg}-Q0FJLZ&w&gz^4QEj;a2Ta}DHQr0K${9l>l}lxqBxUt+zhiQ zQ0!*!L7Q$i>{uOK%8UmYM49gz)Na9^tGDx-L!`Mso8z6*Kb-=tZwjF$OFH29HMLZ{ z4q^dwZdZ!%uX==~$P<*&mme8Oxso*>iWxH%|3Ha8G{O#nL&FwYL(`KSsYB-hm8`vr zgt9+phIVjT^F`qBH`%ttz}dEJjQT&ZU-Idz(}3VS!ur0*vnZk_w~MGM;6w zpQulI(TjZ+LDjFdUY(V+b2V0EZ}J!pPq26oM?g<{49&i@KSTc9-FBX#uO;By zPRLkeGjEc+51G@QEQ*#1zjrlwz{hx$uWs-e-ENM_=i9D`w{Gwz61{%1;WBLbasQx@ zt1an?x0UU{vRS$XFhVq2Kb5x;YcE&DzdUB3idp@R=hZar=WpH3z_((FX9_u-*I%W)`AjnV)EL{ z53S+-tRa@&Db5!CE`1!;NGXp_?+I8lX(~uqQ3n-H@622g9c?>8JYRkOzOFw?7fNh7acK5^hC6dF?^L67DOUC3&!UFbhMna3l?CXI&$uZ5 zrHi1wm~m})(bBKmMRT9oW6GKSjl!7v{@Kpn9tN!y*WqIkH$~!9ev|(tdp_9 z^|Yl46iiSr*tcI=nq6CpJmD0|ZHX-36xGI#`!>7}xELR6FNNZU-fT>?qUow|2oF4f zYpa=QRnmT>I~3{uIf;wMW4gPqrL1VT)N?_Hj8W{9=1pYce3se#GV4)z6mj0VSS^c}eOyEEBmjQSMc>m@H zv(-?BZE)bV?P$B>CjCP0rJ)PJc)frg$8V#b1g+DSQ{p3#SA7}scz;PjSf(ZN%xSqg z(t3vJbba6=G^yFpKzKQ@kS^ku(q5Zzczyh8d3oE=!^%cTSYLkuDl8)QeXgCk!PVq? z;oWNO<>}f$dcCj_lTfwqJF@$a4FQ3ohmXsTnTg8kn4$~D`OqR8D=S(7(X=N= zpIWV1(jWOZqs|{as+Fc%PMe1+s&}R@AK*ur!D~W$<^Q8s%^l$4FV$>-2jxVN zO9@6O$(_&&=xRdZkn~hCyutcmT4m-OCyzvhPFie1XJY9*8Nrd!XCM=_9*M+(+f7zL zK^W|`PZ=Asjs*n^W2ML=?DJ-{T%LJaoS!uh`?WmoV`*a+I1I$b=QF2JXVYFK5u=`s z^jr6=AYo&(g_)K(WyUz}5q~a7Ls>_G6Zh-{o1WN4>)f>mDX#MM*U7E&$x`jz69?V5 z+nJISiN3>u4$a5bZMkFwRJM{4+$Eg>m$Xu^lQxz%Q8w!O!&F@8X0sjqw09}g+1~=a z3~or>B`9C}7tJU#SjrWrXiOx-vp%&b6yuBsj90oM%`F{tiCE+Rnc}?ps(Gq79%w9x z2 za3(WWT1+o!6W+V1i2s1*aP+C?-^V|Y<3F4L0vvx22DQ~xhAj%>wH|0!g&^#8@rco> zv0)(>fRP0*kd;qQE24Y0&_f}T^|-g!F*VldwsDe?v-V9wLX^R7*}kG<-W`6?T<6l^ z#%GOUDux+6tMwrp#}&(NJBE}ht7eC0eug5xL}Eog*Q6>N?H-mied`wjZ^a^)7n@?{ z%3=~Eys7#!0!23-86SlXT>IVLT8!_kA8|)=g>4nBUnIwi8jzsB@(T>)eIP8_tfJ|_ zxRh2%^}n3wyQk{oCqBm{0(QiEk@fjDpW=_3Xfv?hvOba>uF&Obv^2rE3(_DIzXJr-1!VPJ7NiIQLnzXyASn>t_)V?eYe6sJ; zADg~^jiJ06?%{GTN{ze7_F?gihggwO+q7w+ES&$!uiUC%Kg+nH$z>=>`EJtlV|n=3 zDMqbe|6fQ9h#D{E3H(vk5ZpTu*AQQ<iE8!ZVd-mIlFH2mwX>W7Ht8}ORJ#)v>P-k9>T9d2Nmemv@ zUXrKs*m7v+!_o@7+q0nM6-7%A4Vi4g6MDJP8gx1c?8S_BjBr}*S1Ca-26zIjQ%!FL zlZ;L=&z?IzTCbkfbi<$=Sh!TBu!Rm+eIt7or!(`hEYJ#%ogb~)deF%Khn_>Epq^7? zTbe*3I47-K;ubTSan(ubmh}XMuMO@V@MT;fAU(Hz%A^vMHQ2^z{&kQpxGo+MnA(T| zo(wlDk<=rTycN!W!Pm{DFl2G=IbwJ7C`UR_bwsp2q8s2hm`1v#kx>cy9DY*S$3kS> zLY~2zor0`#z>mr|>_fG`s7N(ayOg42(*#pIOZ&|{WKpg#vccAlgeI%!fqdQ^p>-N# zR?f}e7=QAjb!%SR`POh$P}SEYW<8Sf?0d4KUbF+7-6+toAG?1|{RvLSUW~JPtr}6WkU7OM~k2Jx}-j?;;7tG`@72 zS}xi<8&jY1=)&)AK1TDUD7W6;hv;-I^?4EZ6V+tMikGgpmNhV>Vue}@^ID!yfSAY-wA7ouS$nsKmDA7|)NewPkn8g?6ku;|QAA zfm+~|N-{3w`AFVij|#8O@tExi-Mo7EF+xd)PzOQt7a2t}7;A=ON6ceaRyUUp*@{@j z(b)P@cwnu-F8l9YFzS71cVs@a`q!cdqiv(0;Q6p7ldKpL?7XX$8x!q=kMq$TF!Ic84hKJOnk_-?Lz}v1jC#S`b<|mRps=DtUN<8!%k1 zQtV7yJHL>z=M*LN@#qM0u*6NI;!-7RUemsaM@{qH{k5)r2$fmDK8g}Vf0{CkORVk( zHzeMES^~V|t1>MKxO#Hr$rnSLKh&)tST%yi$4%B~CD|5Pc<>ApTJ-WW{u%-rXZ>9E?U&FFoXU25>=;Bu5M6R9XB3ft&euyEY}9&JEt%~kNNs7qougqUL^izS;#!S z;^}q2UQ6HJ!s_gRLxkDF{zpCY@wLqSHFB47U3p@rLCrx#g#q)yDb{-E>4eMFwITg7 zyWR8YYmzWonC+s1;1d6-bjf%?4vMcNPIS=8`Eou5h?G=8M7^@%6oC37&2T9``i zA`8J$I>Rl461NTT1h(yRLhC^Bw&$L^igtnR^bhzAb z;xUj<3+s;M-Qo<Oetq=Zma04UhO`?*2PA{ge{B*jAwK}1@m`YFI z-`%xgSn+!s{1rI~+HPRTKQ|I|G8e;xzc5d*Kyg%6@7?rEM?H(?R>7%tAjtqu-gEOi zq+CXH^_|db@{<_gHX>R$pGd~xM>*2>*id9HlL;GCEsd~SWaP{uQu zrrVOjihx`qs=*i9+wkqTy0l&E+r%gN0=vW>tf{xEk@1I-o^6lfdR~ z_)T0z-{P6@gFp3X6QU~7eR{Z`=HuX}QILQ$=)qJxuq`>pQgOjDOTuULwZ~dqoXdf| zLaRvcjlP7_S^z0<$j!>F_6rDQ^H?L&i2#z$`Nb}2D@rLt%d)#)i}8K3T8YGhF2P`_ z@*TWxo5-~AZX^DVKY=Zm-4Om^_h>mM5QB|QlG0xywU|C1AJ_Jk_+KmHB-KU~w z>jupi*Yy`Uh%*qKvVtV^8F{?C7k*imJkIW&F5xuUBWrILxONY2Zu6vmT92zoX$oWD zV1Q}hJq5x`HIcvZ+8L>gC_eU{oHkt{1k?(z%F-_qTE1=>Lmys#Nrx`GXGEOX z!C_Lskuo_BCWMqyTE-1L{k(e|DhaLnllUe;dk1yaR7&E{R~pLf*F05DL0p7~O9|&1 zp@f5<@dMH2s(C^{P1J&b!oDc?y9hM^zLyvy6uvN!BmU# zDureHq%3Xd7tC9_n#CS=#xvKXhIy%@aQaW36=7n*;3nh}$Jjhy;g@St32mlk{DlqO z8t0ixFL)IKup38OPJ>5~eUDG^;*+bUm9w%sGpE&3X%%mC(pM^Z(^fkHcW-A@VPD>> zCrz_sksDd>P^PvwCfjb;Sz5>utVCW^G+_AsX4{F%2uxNOk_}4uxg4wS(A#kOWwkMI=r1lFGa`$w*L)qW%+U4xM_Ghf-7ddP1LK(2PKfNB0AKT~}8HX)H-r1u4t+7~SHo_HAs zhS^){3AYdAAC|wmJk65z2;^4SHG}Bh`Mi180;a|wLLK)Bg%#hqrghrgF^nFO%cd|S zF`a(hqmW6<>z@WAjdzvqp&oV2!og_4GyHc{x*@KXGCxEL z`C|^5QN;U;tTIv<*qL>|lNjqRs6lR-@G`*$eo1VUtBNF2?0okHTQfXEyE6SV=05}c zal@n^VLTsy5AxEOl0Q&rU-#}&sC$oR%P=~ZV8SR$$rk?1lS;T2k}4Vdn7(By7K&F5 zLKLBvpt+zDPI|JG!-0Le)UDf1Xynhj$c}1vVmiR2*>6to1a<}5c-ebG zt(t^K^~ehEQ@J^;Ke-rS-7ZdtJRz=4W#;}jn)P4ANdMAx#a)~&VU7R>MgY5{BLw7Z z1+$X^IYR&pQi7ZuoSYooKu%6BARi}C?_VYUDy;<4fan0+teh#)hV*w~nY?Cc=VfEt!oPJrjz9&G-M%mUDYI66Ix0^ni; zaszn;IC*)vSUK5vS%GZa9BiCi00V}<1W>Yag#ez#1R5{`tnES0mQD~5!1e!D@px5r@t8q z|CL7hpJs%P8pQlxd(8p7+|PBaY@DClVwbUb?nNnx84UbvZ2a@B3Uah{0tgE~_cYAL z#n#RV!1I@M&t3*I=jTp+HbF#10qoiUeE<-^_52t<3uxsD0RW$i$x8wBg}F@41qAr{ zOilSfrl#CnJlv+d{Jh+Je5ODFa}bbMKvY=3lp7+zCBV%OHsj{zGZg@WfgGINU=SEA z05av{;1Mq)zYYPHgasml5Bj8vDzkR`I)0h9-g#<0Cmy zO>|K?Jc!HqaLeG>Uu*{v5NyTJCjNb70sdzfhLbbM(b@fZ@L+HPc`@F;l~$3#_&=%y B$DjZJ literal 0 HcmV?d00001 From ba16b5a66d5018c7aa471d8cfa559c888d64c8b0 Mon Sep 17 00:00:00 2001 From: FFFlora0349 <59624826+FFFlora0349@users.noreply.github.com> Date: Tue, 12 Apr 2022 19:52:26 -0400 Subject: [PATCH 25/25] upload report --- report/report.pdf | Bin 142568 -> 142204 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/report/report.pdf b/report/report.pdf index 0ca55f4d1544a7d4797fe8c89c43cb4af512338c..bd6648e6531991573b5603f8872ba35045ebe658 100644 GIT binary patch delta 10644 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 delta 11020 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