diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..b8d7c525 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.analysis.extraPaths": ["./Bomberman"] +} diff --git a/Bomberman/game.py b/Bomberman/game.py index 9370d941..6442b488 100644 --- a/Bomberman/game.py +++ b/Bomberman/game.py @@ -4,6 +4,11 @@ import pygame import math +x = 1330 +y = 0 +import os +os.environ['SDL_VIDEO_WINDOW_POS'] = "%d,%d" % (x,y) + class Game: """Game class""" diff --git a/Bomberman/world.py b/Bomberman/world.py index 9a4458d2..e121248a 100644 --- a/Bomberman/world.py +++ b/Bomberman/world.py @@ -389,3 +389,5 @@ def update_scores(self): for k,clist in self.characters.items(): for c in clist: self.scores[c.name] = self.scores[c.name] + 1 + + diff --git a/README.md b/README.md index 9c543f00..3102a917 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,6 @@ +# Overview # +We are some Robotics Engineers trying to make an AI for the classical game Bomberman. Was it fun? Yeah. Did it work? Just find out! + # Required Software # To run Bomberman, you'll need Python 3 with the `colorama` and `pygame` diff --git a/teamNN/PriorityQueue.py b/teamNN/PriorityQueue.py new file mode 100644 index 00000000..1e9c5a84 --- /dev/null +++ b/teamNN/PriorityQueue.py @@ -0,0 +1,42 @@ +import heapq + +class PriorityQueue: + + def __init__(self): + """ + Class constructor. + """ + self.elements = [] + + def empty(self): + """ + Returns True if the queue is empty, False otherwise. + """ + return len(self.elements) == 0 + + def put(self, element, priority): + """ + Puts an element in the queue. + :param element [any type] The element. + :param priority [int or float] The priority. + """ + for i in range(0, len(self.elements)): + it = self.elements[i] + if (it[1] == element): + if (it[0] > priority): + self.elements[i] = (priority, element) + heapq.heapify(self.elements) + return + heapq.heappush(self.elements, (priority, element)) + + def get(self): + """ + Returns the element with the top priority. + """ + return heapq.heappop(self.elements)[1] + + def get_queue(self): + """ + Returns the content of the queue as a list. + """ + return self.elements \ No newline at end of file diff --git a/teamNN/interactivecharacter.py b/teamNN/interactivecharacter.py index cd1c4acd..a53be832 100644 --- a/teamNN/interactivecharacter.py +++ b/teamNN/interactivecharacter.py @@ -1,10 +1,12 @@ # This is necessary to find the main code import sys + sys.path.insert(0, '../bomberman') # Import necessary stuff from entity import CharacterEntity from colorama import Fore, Back + class InteractiveCharacter(CharacterEntity): def do(self, wrld): diff --git a/teamNN/project1/map.txt b/teamNN/project1/map.txt index 35db2451..643952c1 100644 --- a/teamNN/project1/map.txt +++ b/teamNN/project1/map.txt @@ -1,6 +1,6 @@ max_time 5000 -bomb_time 10 -expl_duration 2 +bomb_time 1 +expl_duration 1 expl_range 4 +--------+ | | diff --git a/teamNN/project1/minimax.py b/teamNN/project1/minimax.py new file mode 100644 index 00000000..0670e57b --- /dev/null +++ b/teamNN/project1/minimax.py @@ -0,0 +1,126 @@ +# This is necessary to find the main code +import sys + +sys.path.insert(0, '../bomberman') + +sys.path.insert(1, '../') +from utility import * + + +class AI(): + isExpectimax: bool = False + reccursionDepth: int = 3 + reward_max: int = 50 + reward_min: int = -50 + nodes_explored_count: int = 0 + + def get_next_move(self, wrld, alpha=-float("inf"), beta=float("inf")): + # if there are no monsters, just go to the exit + if len(wrld.monsters) == 0: + path = a_star(wrld, (wrld.exitcell[0], wrld.exitcell[1]), + (character_location(wrld)[0], character_location(wrld)[1])) + if path is None: # Blocked in by explosion, wait until it goes away + return character_location(wrld) + return path[1] + # Get the next move using minimax with alpha-beta pruning + possible_moves = eight_neighbors(wrld, character_location(wrld)[0], character_location(wrld)[1]) + # possible_moves.append(character_location(wrld)) + prioritize_moves_for_self(wrld, possible_moves) + values = [] + for move in possible_moves: + value = self.get_value_of_state(wrld, move, monster_location(wrld), 0, alpha, beta) + values.append(value) + alpha = max(alpha, value) + if alpha >= beta: + break + print("Pruned", round(self.nodes_explored_count / 9 ** (self.reccursionDepth + 1) * 100), "% of the tree.", + self.nodes_explored_count, "nodes explored.") + self.nodes_explored_count = 0 + if len(values) == 0: + return character_location(wrld) + print(max(values), values) + if min(values) < 0: + print("No good moves") + return possible_moves[values.index(max(values))] + + def get_value_of_state(self, wrld, self_pos, monster_pos, depth, alpha, beta): + self.nodes_explored_count += 1 + if self_pos == wrld.exitcell: + return 300 - depth + + if wrld.explosion_at(self_pos[0], self_pos[1]): + return -100 - depth + + if self_pos in eight_neighbors(wrld, monster_pos[0], monster_pos[1]) or self_pos == monster_pos: + return -100 - depth + + if depth == self.reccursionDepth: + return evaluate_state(wrld, self_pos, monster_pos) - depth + + if depth % 2 == 1: # Max Node (self) + value = -float("inf") + possible_moves = eight_neighbors(wrld, self_pos[0], self_pos[1]) + possible_moves.append(self_pos) + for self_move in possible_moves: + value = max(value, self.get_value_of_state(wrld, self_move, monster_pos, depth + 1, alpha, beta)) + alpha = max(alpha, value) + if alpha >= beta: + break + return value + else: + + if not self.isExpectimax: # If modeling monster as Minimax + value = float("inf") + possible_moves = eight_neighbors(wrld, monster_pos[0], monster_pos[1]) + possible_moves.append(monster_pos) + for monster_move in possible_moves: + value = min(value, self.get_value_of_state(wrld, self_pos, monster_move, depth + 1, alpha, beta)) + beta = min(beta, value) + if alpha >= beta: + break + return value + else: # If Expectimax + value = 0 + probability_total = 0 + possible_moves = eight_neighbors(wrld, monster_pos[0], monster_pos[1]) + possible_moves.append(monster_pos) + for monster_move in possible_moves: + probability = 1 / len(possible_moves) + probability_total += probability + value += self.get_value_of_state(wrld, self_pos, monster_move, depth + 1, alpha, + beta) * probability + remaining_probability = 1 - probability_total + if value + (remaining_probability * self.reward_max) < alpha: + # print("Pruned expected node with remaining probability", remaining_probability) + break + return value + + +def prioritize_moves_for_self(wrld, possible_moves): + # Prioritize moves that are closer to the exit to prune the tree more + possible_moves.sort(key=lambda move: euclidean_distance_to_exit(wrld, move)) + + +def prioritize_moves_for_monster(wrld, possible_moves): + # Prioritize moves that are closer to the exit to prune the tree more + possible_moves.sort(key=lambda move: euclidean_dist(move, character_location(wrld))) + + +def evaluate_state(wrld, characterLocation=None, monsterLocation=None): + """Returns a value for the current world state. + wrld: World object + returns: float""" + # print("Evaluating state with character location: " + str(characterLocation) + " and monster location: " + str(monsterLocation)) + if characterLocation is None: + characterLocation = character_location(wrld) + if monsterLocation is None: + monsterLocation = monster_location(wrld) + + number_of_move_options = len(eight_neighbors(wrld, characterLocation[0], characterLocation[1])) + distance_to_exit = a_star_distance(wrld, characterLocation, wrld.exitcell) + if len(wrld.monsters) == 0: + return int(distance_to_exit * 5) + number_of_move_options * 10 + distance_to_monster = a_star_distance(wrld, characterLocation, monsterLocation) + if distance_to_monster <= 2: # The monster is within one tile away + return -100 + return int((distance_to_monster * 5) - distance_to_exit * 6) + number_of_move_options * 5 diff --git a/teamNN/project1/minimaxnode.py b/teamNN/project1/minimaxnode.py new file mode 100644 index 00000000..626b8c7f --- /dev/null +++ b/teamNN/project1/minimaxnode.py @@ -0,0 +1,91 @@ +# This is necessary to find the main code +import sys + +sys.path.insert(0, '../bomberman') +# Import necessary stuff +from testcharacter import CharacterEntity +from colorama import Fore, Back +from PriorityQueue import PriorityQueue +from sys import maxsize +from utility import * + + +class Node(object): + + def __init__(self, depth, player,wrld,position,value=0): + self.depth = depth + self.player = player + self.value = value + self.wrld = wrld + self.position = position + self.distancetogoal = a_star_distance_to_exit(wrld,position) + #print("checkone " + str(position)) + #print("checktwo: " + str(player)) + self.children = [] + self.makechildren(self.wrld) + + def makechildren(self,wrld): + if self.depth >= 0: + neighbordlist = eight_neighbors(wrld, character_location(wrld)[0], character_location(wrld)[1]) + #print("check3 " + str(character_location(wrld)[0])) + #print("check3 " + str(character_location(wrld)[1])) + for neighbord in neighbordlist: + + newdistance = self.distancetogoal - a_star_distance_to_exit(wrld,neighbord) + #print(neighbord) + #print(newdistance) + newpostion = (neighbord[0], neighbord[1]) + self.children.append(Node(self.depth - 1, - self.player, wrld,newpostion, evaluateState(wrld,newpostion))) + + + +def evaluateState(wrld, pos): + exitDist = a_star_distance_to_exit(wrld,pos) + mosnterDist = euclidean_distance_to_monster(wrld,pos) + print("Pos: " + str(pos)) + print("Exit Dist: " + str(exitDist)) + print("Monster Dist: " + str(mosnterDist)) + print("Value: " + str((mosnterDist * 0.7) - exitDist)) + if exitDist == 0: + + return maxsize + return (mosnterDist * 0.7) - exitDist + +def minimax(node, depth, player): + + #print(node) + if (depth == 0) or (abs(node.value == maxsize)): # or game wine, game lose + return node.value + bestvalue = maxsize * -player + + for i in range(len(node.children)): + child = node.children[i] + value = minimax(child, depth - 1, -player) + if (abs(maxsize * player - value) < abs(maxsize * player - bestvalue)): + bestvalue = value + + return bestvalue + +def getNextMove_MiniMax2(wrld): + depthserach = 2 + currentplayer = -1 #monster + location = character_location(wrld) + #print("movemove") + + #print(character_location(wrld)) + #print(monster_location(wrld)) + + if character_location(wrld) != monster_location(wrld): + + currentplayer *= -1 + node = Node(depthserach,currentplayer,wrld,location) + bestmove = node.position + bestvalue = - currentplayer * maxsize + for i in range(len(node.children)): + child = node.children[i] + value = minimax(child,depthserach,-currentplayer) + if ( abs(currentplayer * maxsize - value)<= abs(currentplayer*maxsize-bestvalue)): + bestvalue = value + bestmove = child.position + print("what is returning: " + str(bestmove)) + return bestmove diff --git a/teamNN/project1/testall.py b/teamNN/project1/testall.py new file mode 100644 index 00000000..587be71f --- /dev/null +++ b/teamNN/project1/testall.py @@ -0,0 +1,144 @@ +# This is necessary to find the main code +import sys + +import pygame + +from monsters.selfpreserving_monster import SelfPreservingMonster +from teamNN.interactivecharacter import InteractiveCharacter + +sys.path.insert(0, '../../bomberman') +sys.path.insert(1, '..') + +# Import necessary stuff +import random +from game import Game +from monsters.stupid_monster import StupidMonster + +# TODO This is your code! +sys.path.insert(1, '../teamNN') +from testcharacter import TestCharacter + +numberOfGames = 10 # Number of games to play for each variant +seedOffset = 10 # Offset for the random seed +waitTimeMS = 1000 # Wait time between frames in ms + +pygame.display.set_caption('V1 G1 LastS: ' + str(0)) +g = Game.fromfile('map.txt') +g.add_character(TestCharacter("me", # name + "C", # avatar + 0, 0 # position + )) +g.go(waitTimeMS) +score = g.world.scores['me'] +pygame.display.set_caption('V2 G1 S: ' + str(score)) +scores2 = [] +for i in range(numberOfGames): + random.seed(seedOffset + i) + g = Game.fromfile('map.txt') + g.add_monster(StupidMonster("stupid", # name + "S", # avatar + 3, 9 # position + )) + g.add_character(TestCharacter("me", # name + "C", # avatar + 0, 0 # position + )) + + g.go(waitTimeMS) + pygame.display.set_caption("V2 G%i S:%i" % (i + 2, g.world.scores['me'])) + scores2.append(g.world.scores['me']) + +scores3 = [] +pygame.display.set_caption('V3 G1 S: ' + str(scores2[numberOfGames - 1])) +for i in range(numberOfGames): + random.seed(seedOffset + i) + g = Game.fromfile('map.txt') + g.add_monster(SelfPreservingMonster("selfpreserving", # name + "S", # avatar + 3, 9, # position + 1 # detection range + )) + + g.add_character(TestCharacter("me", # name + "C", # avatar + 0, 0 # position + )) + + g.go(waitTimeMS) + pygame.display.set_caption("V3 G%i S:%i" % (i + 2, g.world.scores['me'])) + scores3.append(g.world.scores['me']) + +scores4 = [] +pygame.display.set_caption('V4 G1 S: ' + str(scores3[numberOfGames - 1])) +for i in range(numberOfGames): + random.seed(seedOffset + i) + g = Game.fromfile('map.txt') + g.add_monster(SelfPreservingMonster("aggressive", # name + "A", # avatar + 3, 13, # position + 2 # detection range + )) + + g.add_character(TestCharacter("me", # name + "C", # avatar + 0, 0 # position + )) + + g.go(waitTimeMS) + pygame.display.set_caption("V4 G%i S:%i" % (i + 2, g.world.scores['me'])) + scores4.append(g.world.scores['me']) + +scores5 = [] +pygame.display.set_caption('V5 G1 S: ' + str(scores4[numberOfGames - 1])) +for i in range(numberOfGames): + random.seed(seedOffset + i) + g = Game.fromfile('map.txt') + g.add_monster(StupidMonster("stupid", # name + "S", # avatar + 3, 5, # position + )) + g.add_monster(SelfPreservingMonster("aggressive", # name + "A", # avatar + 3, 13, # position + 1 # detection range + )) + + g.add_character(TestCharacter("me", # name + "C", # avatar + 0, 0 # position + )) + + g.go(waitTimeMS) + pygame.display.set_caption("V5 G%i S:%i" % (i + 2, g.world.scores['me'])) + scores5.append(g.world.scores['me']) + +print("--- Variant 1 ---") +print("Score: ", score) +print() + +average_score = sum(scores2) / len(scores2) +print("--- Variant 2 ---") +print("Played ", numberOfGames, " games with an average score of ", average_score, "Won ", + len([score for score in scores2 if score > 0]), " games / ", numberOfGames) +print("Scores: ", scores2) +print() + +print("--- Variant 3 ---") +average_score = sum(scores3) / len(scores3) +print("Played ", numberOfGames, " games with an average score of ", average_score, "Won ", + len([score for score in scores3 if score > 0]), " games / ", numberOfGames) +print("Scores: ", scores3) +print() + +print("--- Variant 4 ---") +average_score = sum(scores4) / len(scores4) +print("Played ", numberOfGames, " games with an average score of ", average_score, "Won ", + len([score for score in scores4 if score > 0]), " games / ", numberOfGames) +print("Scores: ", scores4) +print() + +print("--- Variant 5 ---") +average_score = sum(scores5) / len(scores5) +print("Played ", numberOfGames, " games with an average score of ", average_score, "Won ", + len([score for score in scores5 if score > 0]), " games / ", numberOfGames) +print("Scores: ", scores5) diff --git a/teamNN/project1/variant1.py b/teamNN/project1/variant1.py index 54389be8..1770f806 100644 --- a/teamNN/project1/variant1.py +++ b/teamNN/project1/variant1.py @@ -1,5 +1,6 @@ # This is necessary to find the main code import sys + sys.path.insert(0, '../../bomberman') sys.path.insert(1, '..') @@ -7,10 +8,10 @@ from game import Game # TODO This is your code! -sys.path.insert(1, '../teamNN') +sys.path.insert(2, '../teamNN') # Uncomment this if you want the empty test character -#from testcharacter import TestCharacter +from testcharacter import TestCharacter # Uncomment this if you want the interactive character from interactivecharacter import InteractiveCharacter @@ -21,21 +22,21 @@ # TODO Add your character # Uncomment this if you want the test character -# g.add_character(TestCharacter("me", # name -# "C", # avatar -# 0, 0 # position -# )) +g.add_character(TestCharacter("me", # name + "C", # avatar + 0, 0 # position + )) # Uncomment this if you want the interactive character -g.add_character(InteractiveCharacter("me", # name - "C", # avatar - 0, 0 # position -)) +# g.add_character(InteractiveCharacter("me", # name +# "C", # avatar +# 0, 0 # position +# )) # Run! # Use this if you want to press ENTER to continue at each step -# g.go(0) +#g.go(0) # Use this if you want to proceed automatically -g.go(1) +g.go(300) diff --git a/teamNN/project1/variant2.py b/teamNN/project1/variant2.py index 306f08e8..ee863f10 100644 --- a/teamNN/project1/variant2.py +++ b/teamNN/project1/variant2.py @@ -1,5 +1,7 @@ # This is necessary to find the main code import sys + + sys.path.insert(0, '../../bomberman') sys.path.insert(1, '..') @@ -11,20 +13,52 @@ # TODO This is your code! sys.path.insert(1, '../teamNN') from testcharacter import TestCharacter +from interactivecharacter import InteractiveCharacter + # Create the game -random.seed(123) # TODO Change this if you want different random choices -g = Game.fromfile('map.txt') -g.add_monster(StupidMonster("stupid", # name - "S", # avatar - 3, 9 # position -)) - -# TODO Add your character -g.add_character(TestCharacter("me", # name - "C", # avatar - 0, 0 # position -)) - -# Run! -g.go() +# random.seed(123) # TODO Change this if you want different random choices +# g = Game.fromfile('map.txt') +# g.add_monster(StupidMonster("stupid", # name +# "S", # avatar +# 3, 9 # position +# )) +# +# # TODO Add your character +# # g.add_character(InteractiveCharacter("me", # name +# # "C", # avatar +# # 0, 0 # position +# # )) +# +# # Uncomment this if you want the test character +# g.add_character(TestCharacter("me", # name +# "C", # avatar +# 0, 0 # position +# )) +# +# # Run! +# g.go(200) + + +numberOfGames = 10 +scores = [] +for i in range(numberOfGames): + random.seed(i) + g = Game.fromfile('map.txt') + g.add_monster(StupidMonster("stupid", # name + "S", # avatar + 3, 9 # position + )) + g.add_character(TestCharacter("me", # name + "C", # avatar + 0, 0 # position + )) + + g.go(100) + scores.append(g.world.scores['me']) + +average_score = sum(scores) / len(scores) +print("Played ", numberOfGames, " games with an average score of ", average_score) +# Print the number of score where the score is over 0 +print("Number of games won: ", len([score for score in scores if score > 0])) +print("Scores: ", scores) diff --git a/teamNN/project1/variant3.py b/teamNN/project1/variant3.py index 3d229181..a11fa3eb 100644 --- a/teamNN/project1/variant3.py +++ b/teamNN/project1/variant3.py @@ -1,5 +1,8 @@ # This is necessary to find the main code import sys + +#from teamNN.interactivecharacter import InteractiveCharacter + sys.path.insert(0, '../../bomberman') sys.path.insert(1, '..') @@ -13,19 +16,48 @@ from testcharacter import TestCharacter # Create the game -random.seed(123) # TODO Change this if you want different random choices -g = Game.fromfile('map.txt') -g.add_monster(SelfPreservingMonster("selfpreserving", # name - "S", # avatar - 3, 9, # position - 1 # detection range -)) - -# TODO Add your character -g.add_character(TestCharacter("me", # name - "C", # avatar - 0, 0 # position -)) - -# Run! -g.go() +# random.seed(123) # TODO Change this if you want different random choices +# g = Game.fromfile('map.txt') +# g.add_monster(SelfPreservingMonster("selfpreserving", # name +# "S", # avatar +# 3, 9, # position +# 1 # detection range +# )) +# +# # TODO Add your character +# g.add_character(TestCharacter("me", # name +# "C", # avatar +# 0, 0 # position +# )) +# # g.add_character(InteractiveCharacter("me", # name +# # "C", # avatar +# # 0, 0 # position +# # )) +# +# # Run! +# g.go(200) + +numberOfGames = 1 +scores = [] +for i in range(numberOfGames): + random.seed(14) + g = Game.fromfile('map.txt') + g.add_monster(SelfPreservingMonster("selfpreserving", # name + "S", # avatar + 3, 9, # position + 1 # detection range + )) + + g.add_character(TestCharacter("me", # name + "C", # avatar + 0, 0 # position + )) + + g.go(1000) + scores.append(g.world.scores['me']) + +average_score = sum(scores) / len(scores) +print("Played ", numberOfGames, " games with an average score of ", average_score) +# Print the number of score where the score is over 0 +print("Number of games won: ", len([score for score in scores if score > 0])) +print("Scores: ", scores) diff --git a/teamNN/project1/variant4.py b/teamNN/project1/variant4.py index 81e4a631..9331fc58 100644 --- a/teamNN/project1/variant4.py +++ b/teamNN/project1/variant4.py @@ -1,5 +1,8 @@ # This is necessary to find the main code import sys + +from teamNN.interactivecharacter import InteractiveCharacter + sys.path.insert(0, '../../bomberman') sys.path.insert(1, '..') @@ -13,19 +16,50 @@ from testcharacter import TestCharacter # Create the game -random.seed(123) # TODO Change this if you want different random choices -g = Game.fromfile('map.txt') -g.add_monster(SelfPreservingMonster("aggressive", # name - "A", # avatar - 3, 13, # position - 2 # detection range -)) - -# TODO Add your character -g.add_character(TestCharacter("me", # name - "C", # avatar - 0, 0 # position -)) - -# Run! -g.go() +# random.seed(3) # TODO Change this if you want different random choices +# g = Game.fromfile('map.txt') +# g.add_monster(SelfPreservingMonster("aggressive", # name +# "A", # avatar +# 3, 13, # position +# 2 # detection range +# )) +# +# # TODO Add your character +# # g.add_character(InteractiveCharacter("me", # name +# # "C", # avatar +# # 0, 0 # position +# # )) +# +# # Uncomment this if you want the test character +# g.add_character(TestCharacter("me", # name +# "C", # avatar +# 0, 0 # position +# )) +# # Run! +# g.go(200) + +# Create the game +numberOfGames = 1 +scores = [] +for i in range(numberOfGames): + random.seed(17) + g = Game.fromfile('map.txt') + g.add_monster(SelfPreservingMonster("aggressive", # name + "A", # avatar + 3, 13, # position + 2 # detection range + )) + + g.add_character(TestCharacter("me", # name + "C", # avatar + 0, 0 # position + )) + + g.go(1000) + scores.append(g.world.scores['me']) + +average_score = sum(scores) / len(scores) +print("Played ", numberOfGames, " games with an average score of ", average_score) +# Print the number of score where the score is over 0 +print("Number of games won: ", len([score for score in scores if score > 0])) +print("Scores: ", scores) diff --git a/teamNN/project1/variant5.py b/teamNN/project1/variant5.py index 807d4942..ab93f5a3 100644 --- a/teamNN/project1/variant5.py +++ b/teamNN/project1/variant5.py @@ -1,5 +1,6 @@ # This is necessary to find the main code import sys + sys.path.insert(0, '../../bomberman') sys.path.insert(1, '..') @@ -14,23 +15,31 @@ from testcharacter import TestCharacter # Create the game -random.seed(123) # TODO Change this if you want different random choices -g = Game.fromfile('map.txt') -g.add_monster(StupidMonster("stupid", # name - "S", # avatar - 3, 5, # position -)) -g.add_monster(SelfPreservingMonster("aggressive", # name - "A", # avatar - 3, 13, # position - 1 # detection range -)) +numberOfGames = 1 +scores = [] +for i in range(numberOfGames): + random.seed(14) + g = Game.fromfile('map.txt') + g.add_monster(StupidMonster("stupid", # name + "S", # avatar + 3, 5, # position + )) + g.add_monster(SelfPreservingMonster("aggressive", # name + "A", # avatar + 3, 13, # position + 1 # detection range + )) + + g.add_character(TestCharacter("me", # name + "C", # avatar + 0, 0 # position + )) -# TODO Add your character -g.add_character(TestCharacter("me", # name - "C", # avatar - 0, 0 # position -)) + g.go(500) + scores.append(g.world.scores['me']) -# Run! -g.go() +average_score = sum(scores) / len(scores) +print("Played ", numberOfGames, " games with an average score of ", average_score) +# Print the number of score where the score is over 0 +print("Number of games won: ", len([score for score in scores if score > 0])) +print("Scores: ", scores) diff --git a/teamNN/project2/README.md b/teamNN/project2/README.md index 8cd05c2e..89e47f20 100644 --- a/teamNN/project2/README.md +++ b/teamNN/project2/README.md @@ -1,15 +1,14 @@ # Your goal # In this scenario, you must plan the route of your agent from the top-left -corner to the exit. However, your route is obstructed - you need to use the bomb -to create a path to the exit. +corner to the exit. ## Variant 1: Alone in the world ## In the first variant of this scenario, the world is deterministic and your agent is alone in the environment. -## Variant 2: Random monster ## +## Variant 2: Stupid monster ## In the second variant of this scenario, a stupid monster is present. The monster chooses its next cell uniformly at random among the possible reachable cells. diff --git a/teamNN/project2/map.txt b/teamNN/project2/map.txt index 62dab47c..c502e27c 100644 --- a/teamNN/project2/map.txt +++ b/teamNN/project2/map.txt @@ -1,7 +1,7 @@ max_time 5000 -bomb_time 10 -expl_duration 2 -expl_range 4 +bomb_time 1 +expl_duration 1 +expl_range 10 +--------+ | | | | diff --git a/teamNN/project2/minimax.py b/teamNN/project2/minimax.py new file mode 100644 index 00000000..0670e57b --- /dev/null +++ b/teamNN/project2/minimax.py @@ -0,0 +1,126 @@ +# This is necessary to find the main code +import sys + +sys.path.insert(0, '../bomberman') + +sys.path.insert(1, '../') +from utility import * + + +class AI(): + isExpectimax: bool = False + reccursionDepth: int = 3 + reward_max: int = 50 + reward_min: int = -50 + nodes_explored_count: int = 0 + + def get_next_move(self, wrld, alpha=-float("inf"), beta=float("inf")): + # if there are no monsters, just go to the exit + if len(wrld.monsters) == 0: + path = a_star(wrld, (wrld.exitcell[0], wrld.exitcell[1]), + (character_location(wrld)[0], character_location(wrld)[1])) + if path is None: # Blocked in by explosion, wait until it goes away + return character_location(wrld) + return path[1] + # Get the next move using minimax with alpha-beta pruning + possible_moves = eight_neighbors(wrld, character_location(wrld)[0], character_location(wrld)[1]) + # possible_moves.append(character_location(wrld)) + prioritize_moves_for_self(wrld, possible_moves) + values = [] + for move in possible_moves: + value = self.get_value_of_state(wrld, move, monster_location(wrld), 0, alpha, beta) + values.append(value) + alpha = max(alpha, value) + if alpha >= beta: + break + print("Pruned", round(self.nodes_explored_count / 9 ** (self.reccursionDepth + 1) * 100), "% of the tree.", + self.nodes_explored_count, "nodes explored.") + self.nodes_explored_count = 0 + if len(values) == 0: + return character_location(wrld) + print(max(values), values) + if min(values) < 0: + print("No good moves") + return possible_moves[values.index(max(values))] + + def get_value_of_state(self, wrld, self_pos, monster_pos, depth, alpha, beta): + self.nodes_explored_count += 1 + if self_pos == wrld.exitcell: + return 300 - depth + + if wrld.explosion_at(self_pos[0], self_pos[1]): + return -100 - depth + + if self_pos in eight_neighbors(wrld, monster_pos[0], monster_pos[1]) or self_pos == monster_pos: + return -100 - depth + + if depth == self.reccursionDepth: + return evaluate_state(wrld, self_pos, monster_pos) - depth + + if depth % 2 == 1: # Max Node (self) + value = -float("inf") + possible_moves = eight_neighbors(wrld, self_pos[0], self_pos[1]) + possible_moves.append(self_pos) + for self_move in possible_moves: + value = max(value, self.get_value_of_state(wrld, self_move, monster_pos, depth + 1, alpha, beta)) + alpha = max(alpha, value) + if alpha >= beta: + break + return value + else: + + if not self.isExpectimax: # If modeling monster as Minimax + value = float("inf") + possible_moves = eight_neighbors(wrld, monster_pos[0], monster_pos[1]) + possible_moves.append(monster_pos) + for monster_move in possible_moves: + value = min(value, self.get_value_of_state(wrld, self_pos, monster_move, depth + 1, alpha, beta)) + beta = min(beta, value) + if alpha >= beta: + break + return value + else: # If Expectimax + value = 0 + probability_total = 0 + possible_moves = eight_neighbors(wrld, monster_pos[0], monster_pos[1]) + possible_moves.append(monster_pos) + for monster_move in possible_moves: + probability = 1 / len(possible_moves) + probability_total += probability + value += self.get_value_of_state(wrld, self_pos, monster_move, depth + 1, alpha, + beta) * probability + remaining_probability = 1 - probability_total + if value + (remaining_probability * self.reward_max) < alpha: + # print("Pruned expected node with remaining probability", remaining_probability) + break + return value + + +def prioritize_moves_for_self(wrld, possible_moves): + # Prioritize moves that are closer to the exit to prune the tree more + possible_moves.sort(key=lambda move: euclidean_distance_to_exit(wrld, move)) + + +def prioritize_moves_for_monster(wrld, possible_moves): + # Prioritize moves that are closer to the exit to prune the tree more + possible_moves.sort(key=lambda move: euclidean_dist(move, character_location(wrld))) + + +def evaluate_state(wrld, characterLocation=None, monsterLocation=None): + """Returns a value for the current world state. + wrld: World object + returns: float""" + # print("Evaluating state with character location: " + str(characterLocation) + " and monster location: " + str(monsterLocation)) + if characterLocation is None: + characterLocation = character_location(wrld) + if monsterLocation is None: + monsterLocation = monster_location(wrld) + + number_of_move_options = len(eight_neighbors(wrld, characterLocation[0], characterLocation[1])) + distance_to_exit = a_star_distance(wrld, characterLocation, wrld.exitcell) + if len(wrld.monsters) == 0: + return int(distance_to_exit * 5) + number_of_move_options * 10 + distance_to_monster = a_star_distance(wrld, characterLocation, monsterLocation) + if distance_to_monster <= 2: # The monster is within one tile away + return -100 + return int((distance_to_monster * 5) - distance_to_exit * 6) + number_of_move_options * 5 diff --git a/teamNN/project2/minimaxnode.py b/teamNN/project2/minimaxnode.py new file mode 100644 index 00000000..7fbb8ff3 --- /dev/null +++ b/teamNN/project2/minimaxnode.py @@ -0,0 +1,91 @@ +# This is necessary to find the main code +import sys + +sys.path.insert(0, '../bomberman') +# Import necessary stuff +from testcharacter import CharacterEntity +from colorama import Fore, Back +from PriorityQueue import PriorityQueue +from sys import maxsize +from utility import * + + +class Node(object): + + def __init__(self, depth, player,wrld,position,value=0): + self.depth = depth + self.player = player + self.value = value + self.wrld = wrld + self.position = position + self.distancetogoal = a_star_distance_to_exit(wrld,position) + #print("checkone " + str(position)) + #print("checktwo: " + str(player)) + self.children = [] + self.makechildren(self.wrld) + + def makechildren(self,wrld): + if self.depth >= 0: + neighbordlist = eight_neighbors(wrld, character_location(wrld)[0], character_location(wrld)[1]) + #print("check3 " + str(character_location(wrld)[0])) + #print("check3 " + str(character_location(wrld)[1])) + for neighbord in neighbordlist: + + newdistance = self.distancetogoal - a_star_distance_to_exit(wrld,neighbord) + #print(neighbord) + #print(newdistance) + newpostion = (neighbord[0], neighbord[1]) + self.children.append(Node(self.depth - 1, - self.player, wrld,newpostion, evaluateState(wrld,newpostion))) + + + +def evaluateState(wrld, pos): + exitDist = a_star_distance_to_exit(wrld, pos) + mosnterDist = euclidean_distance_to_monster(wrld,pos) + print("Pos: " + str(pos)) + print("Exit Dist: " + str(exitDist)) + print("Monster Dist: " + str(mosnterDist)) + print("Value: " + str((mosnterDist * 0.7) - exitDist)) + if exitDist == 0: + + return maxsize + return (mosnterDist * 0.7) - exitDist + +def minimax(node, depth, player): + + #print(node) + if (depth == 0) or (abs(node.value == maxsize)): # or game wine, game lose + return node.value + bestvalue = maxsize * -player + + for i in range(len(node.children)): + child = node.children[i] + value = minimax(child, depth - 1, -player) + if (abs(maxsize * player - value) < abs(maxsize * player - bestvalue)): + bestvalue = value + + return bestvalue + +def getNextMove_MiniMax2(wrld): + depthserach = 2 + currentplayer = -1 #monster + location = character_location(wrld) + #print("movemove") + + #print(character_location(wrld)) + #print(monster_location(wrld)) + + if character_location(wrld) != monster_location(wrld): + + currentplayer *= -1 + node = Node(depthserach,currentplayer,wrld,location) + bestmove = node.position + bestvalue = - currentplayer * maxsize + for i in range(len(node.children)): + child = node.children[i] + value = minimax(child,depthserach,-currentplayer) + if ( abs(currentplayer * maxsize - value)<= abs(currentplayer*maxsize-bestvalue)): + bestvalue = value + bestmove = child.position + print("what is returning: " + str(bestmove)) + return bestmove diff --git a/teamNN/project2/testall.py b/teamNN/project2/testall.py new file mode 100644 index 00000000..587be71f --- /dev/null +++ b/teamNN/project2/testall.py @@ -0,0 +1,144 @@ +# This is necessary to find the main code +import sys + +import pygame + +from monsters.selfpreserving_monster import SelfPreservingMonster +from teamNN.interactivecharacter import InteractiveCharacter + +sys.path.insert(0, '../../bomberman') +sys.path.insert(1, '..') + +# Import necessary stuff +import random +from game import Game +from monsters.stupid_monster import StupidMonster + +# TODO This is your code! +sys.path.insert(1, '../teamNN') +from testcharacter import TestCharacter + +numberOfGames = 10 # Number of games to play for each variant +seedOffset = 10 # Offset for the random seed +waitTimeMS = 1000 # Wait time between frames in ms + +pygame.display.set_caption('V1 G1 LastS: ' + str(0)) +g = Game.fromfile('map.txt') +g.add_character(TestCharacter("me", # name + "C", # avatar + 0, 0 # position + )) +g.go(waitTimeMS) +score = g.world.scores['me'] +pygame.display.set_caption('V2 G1 S: ' + str(score)) +scores2 = [] +for i in range(numberOfGames): + random.seed(seedOffset + i) + g = Game.fromfile('map.txt') + g.add_monster(StupidMonster("stupid", # name + "S", # avatar + 3, 9 # position + )) + g.add_character(TestCharacter("me", # name + "C", # avatar + 0, 0 # position + )) + + g.go(waitTimeMS) + pygame.display.set_caption("V2 G%i S:%i" % (i + 2, g.world.scores['me'])) + scores2.append(g.world.scores['me']) + +scores3 = [] +pygame.display.set_caption('V3 G1 S: ' + str(scores2[numberOfGames - 1])) +for i in range(numberOfGames): + random.seed(seedOffset + i) + g = Game.fromfile('map.txt') + g.add_monster(SelfPreservingMonster("selfpreserving", # name + "S", # avatar + 3, 9, # position + 1 # detection range + )) + + g.add_character(TestCharacter("me", # name + "C", # avatar + 0, 0 # position + )) + + g.go(waitTimeMS) + pygame.display.set_caption("V3 G%i S:%i" % (i + 2, g.world.scores['me'])) + scores3.append(g.world.scores['me']) + +scores4 = [] +pygame.display.set_caption('V4 G1 S: ' + str(scores3[numberOfGames - 1])) +for i in range(numberOfGames): + random.seed(seedOffset + i) + g = Game.fromfile('map.txt') + g.add_monster(SelfPreservingMonster("aggressive", # name + "A", # avatar + 3, 13, # position + 2 # detection range + )) + + g.add_character(TestCharacter("me", # name + "C", # avatar + 0, 0 # position + )) + + g.go(waitTimeMS) + pygame.display.set_caption("V4 G%i S:%i" % (i + 2, g.world.scores['me'])) + scores4.append(g.world.scores['me']) + +scores5 = [] +pygame.display.set_caption('V5 G1 S: ' + str(scores4[numberOfGames - 1])) +for i in range(numberOfGames): + random.seed(seedOffset + i) + g = Game.fromfile('map.txt') + g.add_monster(StupidMonster("stupid", # name + "S", # avatar + 3, 5, # position + )) + g.add_monster(SelfPreservingMonster("aggressive", # name + "A", # avatar + 3, 13, # position + 1 # detection range + )) + + g.add_character(TestCharacter("me", # name + "C", # avatar + 0, 0 # position + )) + + g.go(waitTimeMS) + pygame.display.set_caption("V5 G%i S:%i" % (i + 2, g.world.scores['me'])) + scores5.append(g.world.scores['me']) + +print("--- Variant 1 ---") +print("Score: ", score) +print() + +average_score = sum(scores2) / len(scores2) +print("--- Variant 2 ---") +print("Played ", numberOfGames, " games with an average score of ", average_score, "Won ", + len([score for score in scores2 if score > 0]), " games / ", numberOfGames) +print("Scores: ", scores2) +print() + +print("--- Variant 3 ---") +average_score = sum(scores3) / len(scores3) +print("Played ", numberOfGames, " games with an average score of ", average_score, "Won ", + len([score for score in scores3 if score > 0]), " games / ", numberOfGames) +print("Scores: ", scores3) +print() + +print("--- Variant 4 ---") +average_score = sum(scores4) / len(scores4) +print("Played ", numberOfGames, " games with an average score of ", average_score, "Won ", + len([score for score in scores4 if score > 0]), " games / ", numberOfGames) +print("Scores: ", scores4) +print() + +print("--- Variant 5 ---") +average_score = sum(scores5) / len(scores5) +print("Played ", numberOfGames, " games with an average score of ", average_score, "Won ", + len([score for score in scores5 if score > 0]), " games / ", numberOfGames) +print("Scores: ", scores5) diff --git a/teamNN/project2/variant1.py b/teamNN/project2/variant1.py index 6d1d284c..c27991a7 100644 --- a/teamNN/project2/variant1.py +++ b/teamNN/project2/variant1.py @@ -1,5 +1,6 @@ # This is necessary to find the main code import sys + sys.path.insert(0, '../../bomberman') sys.path.insert(1, '..') @@ -7,18 +8,35 @@ from game import Game # TODO This is your code! -sys.path.insert(1, '../teamNN') +sys.path.insert(2, '../teamNN') + +# Uncomment this if you want the empty test character from testcharacter import TestCharacter +# Uncomment this if you want the interactive character +from interactivecharacter import InteractiveCharacter # Create the game g = Game.fromfile('map.txt') # TODO Add your character -g.add_character(TestCharacter("me", # name + +# Uncomment this if you want the test character +g.add_character(TestCharacter("me", # name "C", # avatar 0, 0 # position -)) + )) + +# Uncomment this if you want the interactive character +# g.add_character(InteractiveCharacter("me", # name +# "C", # avatar +# 0, 0 # position +# )) # Run! -g.go() + +# Use this if you want to press ENTER to continue at each step +#g.go(0) + +# Use this if you want to proceed automatically +g.go(100) diff --git a/teamNN/project2/variant2.py b/teamNN/project2/variant2.py index 306f08e8..ee863f10 100644 --- a/teamNN/project2/variant2.py +++ b/teamNN/project2/variant2.py @@ -1,5 +1,7 @@ # This is necessary to find the main code import sys + + sys.path.insert(0, '../../bomberman') sys.path.insert(1, '..') @@ -11,20 +13,52 @@ # TODO This is your code! sys.path.insert(1, '../teamNN') from testcharacter import TestCharacter +from interactivecharacter import InteractiveCharacter + # Create the game -random.seed(123) # TODO Change this if you want different random choices -g = Game.fromfile('map.txt') -g.add_monster(StupidMonster("stupid", # name - "S", # avatar - 3, 9 # position -)) - -# TODO Add your character -g.add_character(TestCharacter("me", # name - "C", # avatar - 0, 0 # position -)) - -# Run! -g.go() +# random.seed(123) # TODO Change this if you want different random choices +# g = Game.fromfile('map.txt') +# g.add_monster(StupidMonster("stupid", # name +# "S", # avatar +# 3, 9 # position +# )) +# +# # TODO Add your character +# # g.add_character(InteractiveCharacter("me", # name +# # "C", # avatar +# # 0, 0 # position +# # )) +# +# # Uncomment this if you want the test character +# g.add_character(TestCharacter("me", # name +# "C", # avatar +# 0, 0 # position +# )) +# +# # Run! +# g.go(200) + + +numberOfGames = 10 +scores = [] +for i in range(numberOfGames): + random.seed(i) + g = Game.fromfile('map.txt') + g.add_monster(StupidMonster("stupid", # name + "S", # avatar + 3, 9 # position + )) + g.add_character(TestCharacter("me", # name + "C", # avatar + 0, 0 # position + )) + + g.go(100) + scores.append(g.world.scores['me']) + +average_score = sum(scores) / len(scores) +print("Played ", numberOfGames, " games with an average score of ", average_score) +# Print the number of score where the score is over 0 +print("Number of games won: ", len([score for score in scores if score > 0])) +print("Scores: ", scores) diff --git a/teamNN/project2/variant3.py b/teamNN/project2/variant3.py index 3d229181..8f1897dc 100644 --- a/teamNN/project2/variant3.py +++ b/teamNN/project2/variant3.py @@ -1,5 +1,8 @@ # This is necessary to find the main code import sys + +#from teamNN.interactivecharacter import InteractiveCharacter + sys.path.insert(0, '../../bomberman') sys.path.insert(1, '..') @@ -13,19 +16,48 @@ from testcharacter import TestCharacter # Create the game -random.seed(123) # TODO Change this if you want different random choices -g = Game.fromfile('map.txt') -g.add_monster(SelfPreservingMonster("selfpreserving", # name - "S", # avatar - 3, 9, # position - 1 # detection range -)) - -# TODO Add your character -g.add_character(TestCharacter("me", # name - "C", # avatar - 0, 0 # position -)) - -# Run! -g.go() +# random.seed(123) # TODO Change this if you want different random choices +# g = Game.fromfile('map.txt') +# g.add_monster(SelfPreservingMonster("selfpreserving", # name +# "S", # avatar +# 3, 9, # position +# 1 # detection range +# )) +# +# # TODO Add your character +# g.add_character(TestCharacter("me", # name +# "C", # avatar +# 0, 0 # position +# )) +# # g.add_character(InteractiveCharacter("me", # name +# # "C", # avatar +# # 0, 0 # position +# # )) +# +# # Run! +# g.go(200) + +numberOfGames = 1 +scores = [] +for i in range(numberOfGames): + random.seed(14) + g = Game.fromfile('map.txt') + g.add_monster(SelfPreservingMonster("selfpreserving", # name + "S", # avatar + 3, 9, # position + 1 # detection range + )) + + g.add_character(TestCharacter("me", # name + "C", # avatar + 0, 0 # position + )) + + g.go(100) + scores.append(g.world.scores['me']) + +average_score = sum(scores) / len(scores) +print("Played ", numberOfGames, " games with an average score of ", average_score) +# Print the number of score where the score is over 0 +print("Number of games won: ", len([score for score in scores if score > 0])) +print("Scores: ", scores) diff --git a/teamNN/project2/variant4.py b/teamNN/project2/variant4.py index 81e4a631..9331fc58 100644 --- a/teamNN/project2/variant4.py +++ b/teamNN/project2/variant4.py @@ -1,5 +1,8 @@ # This is necessary to find the main code import sys + +from teamNN.interactivecharacter import InteractiveCharacter + sys.path.insert(0, '../../bomberman') sys.path.insert(1, '..') @@ -13,19 +16,50 @@ from testcharacter import TestCharacter # Create the game -random.seed(123) # TODO Change this if you want different random choices -g = Game.fromfile('map.txt') -g.add_monster(SelfPreservingMonster("aggressive", # name - "A", # avatar - 3, 13, # position - 2 # detection range -)) - -# TODO Add your character -g.add_character(TestCharacter("me", # name - "C", # avatar - 0, 0 # position -)) - -# Run! -g.go() +# random.seed(3) # TODO Change this if you want different random choices +# g = Game.fromfile('map.txt') +# g.add_monster(SelfPreservingMonster("aggressive", # name +# "A", # avatar +# 3, 13, # position +# 2 # detection range +# )) +# +# # TODO Add your character +# # g.add_character(InteractiveCharacter("me", # name +# # "C", # avatar +# # 0, 0 # position +# # )) +# +# # Uncomment this if you want the test character +# g.add_character(TestCharacter("me", # name +# "C", # avatar +# 0, 0 # position +# )) +# # Run! +# g.go(200) + +# Create the game +numberOfGames = 1 +scores = [] +for i in range(numberOfGames): + random.seed(17) + g = Game.fromfile('map.txt') + g.add_monster(SelfPreservingMonster("aggressive", # name + "A", # avatar + 3, 13, # position + 2 # detection range + )) + + g.add_character(TestCharacter("me", # name + "C", # avatar + 0, 0 # position + )) + + g.go(1000) + scores.append(g.world.scores['me']) + +average_score = sum(scores) / len(scores) +print("Played ", numberOfGames, " games with an average score of ", average_score) +# Print the number of score where the score is over 0 +print("Number of games won: ", len([score for score in scores if score > 0])) +print("Scores: ", scores) diff --git a/teamNN/project2/variant5.py b/teamNN/project2/variant5.py index 96f1503b..dfcf85ba 100644 --- a/teamNN/project2/variant5.py +++ b/teamNN/project2/variant5.py @@ -1,5 +1,6 @@ # This is necessary to find the main code import sys + sys.path.insert(0, '../../bomberman') sys.path.insert(1, '..') @@ -14,23 +15,31 @@ from testcharacter import TestCharacter # Create the game -random.seed(123) # TODO Change this if you want different random choices -g = Game.fromfile('map.txt') -g.add_monster(StupidMonster("stupid", # name - "S", # avatar - 3, 5, # position -)) -g.add_monster(SelfPreservingMonster("aggressive", # name - "A", # avatar - 3, 13, # position - 2 # detection range -)) +numberOfGames = 1 +scores = [] +for i in range(numberOfGames): + random.seed(14) + g = Game.fromfile('map.txt') + g.add_monster(StupidMonster("stupid", # name + "S", # avatar + 3, 5, # position + )) + g.add_monster(SelfPreservingMonster("aggressive", # name + "A", # avatar + 3, 13, # position + 1 # detection range + )) + + g.add_character(TestCharacter("me", # name + "C", # avatar + 0, 0 # position + )) -# TODO Add your character -g.add_character(TestCharacter("me", # name - "C", # avatar - 0, 0 # position -)) + g.go(10) + scores.append(g.world.scores['me']) -# Run! -g.go() +average_score = sum(scores) / len(scores) +print("Played ", numberOfGames, " games with an average score of ", average_score) +# Print the number of score where the score is over 0 +print("Number of games won: ", len([score for score in scores if score > 0])) +print("Scores: ", scores) diff --git a/teamNN/testcharacter.py b/teamNN/testcharacter.py index 6f13216f..55462d08 100644 --- a/teamNN/testcharacter.py +++ b/teamNN/testcharacter.py @@ -1,12 +1,176 @@ # This is necessary to find the main code import sys +from enum import Enum + sys.path.insert(0, '../bomberman') # Import necessary stuff from entity import CharacterEntity -from colorama import Fore, Back -class TestCharacter(CharacterEntity): +sys.path.insert(1, '../teamNN') +from utility import * +from project1.minimax import * + +#Organizing all states +class State(Enum): + START = 1 + PLACE_BOMB = 2 + WAIT_FOR_BOMB = 4 + FAR_FROM_MONSTER = 5 + CLOSE_TO_MONSTER = 6 + EXPLORATION = 7; + +class TestCharacter(CharacterEntity): + waitCount = 0 + bombCoolDown = 0 + stateMachine = State.START + ai = AI() + #Function starts here def do(self, wrld): - # Your code here - pass + monsterNum = len(wrld.monsters.values()) + print("Current State: ", self.stateMachine) + self.bombCoolDown -= 1 + #State Machines starts here + match self.stateMachine: + + #Start state -> When game begins, character will jump into this state + case State.START: + self.move(0, 1) + # #If at corner + # if self.x == 0 and self.y == 2: + self.stateMachine = State.PLACE_BOMB + + #Place bomb, dodge and move onto WaitForBomb state + case State.PLACE_BOMB: + self.place_bomb() + self.dodge_bomb_away_from_monster(wrld) + self.waitCount = 0 + self.stateMachine = State.WAIT_FOR_BOMB + + #Wait till the bomb explodes (stay still) then move on the FarFromMonster state + case State.WAIT_FOR_BOMB: + self.move(0, 0) + self.waitCount += 1 + if self.waitCount > 1: + self.bombCoolDown = 7 + self.stateMachine = State.FAR_FROM_MONSTER + #State where the monster is >= 8 distance from the character + case State.EXPLORATION: + # print("Exploring") + # w = wrld.width + # h = wrld.height + # print(w) + # print(h) + # + # for x in range(7): + # for y in range(17): + # + # if wrld.wall_at(x, y): + # print(x, y) + # path = a_star(wrld, (x, y), character_location(wrld)) + # if path.pop(len(path) - 1) != wrld.exitcell: + # goal = (x, y) + # break + if a_star_distance_to_monster(wrld, (self.x, self.y + 1)) < 8 and len(wrld.monsters) > 0: + self.stateMachine = State.CLOSE_TO_MONSTER + + if is_cell_walkable(wrld, self.x, self.y + 1): + print("cell walkable") + self.move(self.x, self.y + 1) + else: + self.stateMachine = State.PLACE_BOMB + + case State.FAR_FROM_MONSTER: + #Running minimax, depth = 2 + self.ai.reccursionDepth = 2 + self.ai.isExpectimax = False + + print("a_star distance", a_star_distance_to_exit(wrld, character_location(wrld))) + + if a_star_distance_to_exit(wrld, character_location(wrld)) < 0: + print("No path to exit") + self.stateMachine = State.EXPLORATION + + #Generate AI move + nextCell = self.ai.get_next_move(wrld) + #Perform the move + self.move(nextCell[0] - self.x, nextCell[1] - self.y) + print("Score of current world", evaluate_state(wrld, character_location(wrld), monster_location(wrld))) + print("Selected Move: ", nextCell) + + # #If can place bomb -> placebomb + if self.can_place_bomb(nextCell): + self.stateMachine = State.PLACE_BOMB + #If distance to monster < 8 -> close to monster + if a_star_distance_to_monster(wrld, (nextCell[0], nextCell[1])) < 8 and len(wrld.monsters) > 0: + self.stateMachine = State.CLOSE_TO_MONSTER + + case State.CLOSE_TO_MONSTER: + #Using expectimax + self.ai.reccursionDepth = 3 + self.ai.isExpectimax = False + print("a_star distance", a_star_distance_to_exit(wrld, character_location(wrld))) + + if a_star_distance_to_exit(wrld, character_location(wrld)) < 0: + print("No path to exit") + self.stateMachine = State.PLACE_BOMB + nextCell = self.ai.get_next_move(wrld) + self.move(nextCell[0] - self.x, nextCell[1] - self.y) + + print("Score of current world", evaluate_state(wrld, character_location(wrld), monster_location(wrld))) + print("Selected Move: ", nextCell) + + # Evaluating states and current position to place bomb if possible + if evaluate_state(wrld, character_location(wrld), monster_location(wrld)) < -20: + self.stateMachine = State.PLACE_BOMB + if self.can_place_bomb(nextCell): + self.stateMachine = State.PLACE_BOMB + if a_star_distance_to_monster(wrld, (nextCell[0], nextCell[1])) > 8: + self.stateMachine = State.FAR_FROM_MONSTER + + + def can_place_bomb(self, location, ): + """ + Checking if the prev bomb has exploded yet to place a new bomb in an empty location + """ + if not self.bombCoolDown <= 0: + return False + return ((location[0] == 6) and (location[1] == 6 or location[1] == 14)) or ( + (location[0] == 1) and (location[1] == 2 or location[1] == 10)) + + + def dodge_bomb_away_from_monster(self, wrld): + """ + Move away the position where bomb can explode or meet the monster + """ + #Score that the bomb can reach -> hit wall or hit monster + left_up_score = None + right_up_score = None + left_down_score = None + right_down_score = None + + #Calculating each direction score + if is_cell_in_range(wrld, self.x + 1, self.y - 1) and is_cell_walkable(wrld, self.x + 1, self.y - 1): + right_up_score = len(a_star(wrld, (self.x + 1, self.y - 1), monster_location(wrld))) + if is_cell_in_range(wrld, self.x - 1, self.y - 1) and is_cell_walkable(wrld, self.x - 1, self.y - 1): + left_up_score = len(a_star(wrld, (self.x - 1, self.y - 1), monster_location(wrld))) + if is_cell_in_range(wrld, self.x + 1, self.y + 1) and is_cell_walkable(wrld, self.x + 1, self.y + 1): + right_down_score = len(a_star(wrld, (self.x + 1, self.y + 1), monster_location(wrld))) + if is_cell_in_range(wrld, self.x - 1, self.y + 1) and is_cell_walkable(wrld, self.x - 1, self.y + 1): + left_down_score = len(a_star(wrld, (self.x - 1, self.y + 1), monster_location(wrld))) + + #Summarizing up the dodge options to choose the best one + dodge_options = [left_up_score, right_up_score, left_down_score, right_down_score] + dodge_options = [x for x in dodge_options if x is not None] + print(dodge_options) + #Return the move based on the best dodge option (which option that brings the most score) + if len(dodge_options) > 0: + best_move = max(dodge_options) + if best_move == left_up_score: + self.move(-1, -1) + elif best_move == right_up_score: + self.move(1, -1) + elif best_move == left_down_score: + self.move(-1, 1) + elif best_move == right_down_score: + self.move(1, 1) diff --git a/teamNN/utility.py b/teamNN/utility.py new file mode 100644 index 00000000..9464d65a --- /dev/null +++ b/teamNN/utility.py @@ -0,0 +1,399 @@ +""" +Utility functions for the teamNN package. +Import to any file using: from teamNN.utility import * + +Includes: +float euclidean_dist(point_one:tuple, point_two:tuple) +bool is_cell_walkable(wrld:World, x:int, y:int) +tuple[] eight_neighbors(wrld:World, x:int, y:int) +tuple character_location(wrld:World) //Returns First Character +tuple monster_location(wrld:World) //Returns First Monster +int manhattan_distance_to_exit(wrld:World) +int manhattan_distance_to_monster(wrld:World) //Returns Nearest Monster +int euclidean_distance_to_exit(wrld:World) +int euclidean_distance_to_monster(wrld:World) //Returns Nearest Monster + +""" +from PriorityQueue import PriorityQueue + +prevMove = [0, 0] + + +def euclidean_dist(point_one, point_two): + """Returns the euclidean distance between two points. + point_one: (x, y) tuple + point_two: (x, y) tuple + returns: float""" + return ((point_one[0] - point_two[0]) ** 2 + (point_one[1] - point_two[1]) ** 2) ** 0.5 + + +def is_cell_walkable(wrld, x, y): + """Returns True if the cell at (x, y) is walkable. + wrld: World object + x: int + y: int + returns: bool""" + if is_cell_in_range(wrld, x, y): + return wrld.exit_at(x, y) or wrld.empty_at(x, y) or wrld.monsters_at(x, y) or wrld.characters_at(x, y) + return False + + +def is_cell_in_range(wrld, x, y): + """Returns True if the cell at (x, y) is in range. + wrld: World object + x: int + y: int + returns: bool""" + return wrld.width() > x >= 0 and wrld.height() > y >= 0 + + +def eight_neighbors(wrld, x, y): + """ + Returns the walkable 8-neighbors cells of (x,y) in wrld + wrld: World object + x: int + y: int + returns: list of (x, y) tuples + """ + return_list = [] + + if x != 0 and is_cell_walkable(wrld, x - 1, y): + return_list.append((x - 1, y)) + if x != wrld.width() - 1 and is_cell_walkable(wrld, x + 1, y): + return_list.append((x + 1, y)) + if y != 0 and is_cell_walkable(wrld, x, y - 1): + return_list.append((x, y - 1)) + if y != wrld.height() - 1 and is_cell_walkable(wrld, x, y + 1): + return_list.append((x, y + 1)) + if x != 0 and y != 0 and is_cell_walkable(wrld, x - 1, y - 1): + return_list.append((x - 1, y - 1)) + if x != wrld.width() - 1 and y != 0 and is_cell_walkable(wrld, x + 1, y - 1): + return_list.append((x + 1, y - 1)) + if y != wrld.height() - 1 and x != 0 and is_cell_walkable(wrld, x - 1, y + 1): + return_list.append((x - 1, y + 1)) + if x != wrld.width() - 1 and y != wrld.height() - 1 and is_cell_walkable(wrld, x + 1, y + 1): + return_list.append((x + 1, y + 1)) + + return return_list + + +def a_star(wrld, goal=None, start=None): + print("Astar is thinking") + found = False + + if start is None: + start = character_location(wrld) # Start at current position + if goal is None: + goal = wrld.exitcell # Goal is exit cell + + cost_so_far = {start: 0} # Dictionary of costs to get to each cell + came_from = {start: None} # Dictionary of where each cell came from + + frontier = PriorityQueue() # Priority queue of cells to visit + frontier.put(start, 0) + + while not frontier.empty(): + current = frontier.get() + if current == goal: + found = True + break + + successors = identifySuccessors(current, wrld, goal) + + # Check all walkable neighbors of current cell + for successor in successors: + # Calculate cost to get to neighbor - 1 or 1.4 + diagonal = 0 + if successor in cost_so_far and \ + successor[0] - current[0] != 0 and successor[1] - current[1] != 0: + diagonal += 2 + new_cost = cost_so_far[current] + euclidean_dist(current, successor) + diagonal + + # If neighbor has no path or new path is better, update path + if successor not in cost_so_far or new_cost < cost_so_far[successor]: + cost_so_far[successor] = new_cost + priority = new_cost + euclidean_dist(successor, goal) + frontier.put(successor, priority) + came_from[successor] = current + + if not found: + return [start, start] + # Reconstruct path using came_from dictionary + currPos = goal + finalPath = [goal] + while currPos != start: + currPos = came_from[currPos] + finalPath.append(currPos) + + finalPath.reverse() + return finalPath + + +def identifySuccessors(current, wrld, goal): + succesors = [] + for neighbour in eight_neighbors(wrld, current[0], current[1]): + dx = neighbour[0] - current[0] + dy = neighbour[1] - current[1] + direction = [dx, dy] + pointfump = jump(current, direction[0], direction[1], wrld, goal) + if pointfump: + succesors.append(pointfump) + return succesors + + +def jump(current, dx, dy, wrld, goal): + nx = current[0] + dx + ny = current[1] + dy + + if (nx, ny) == goal: + return (nx, ny) + + # Original position + ox = nx + oy = ny + original = (ox, oy) + + if dx != 0 and dy != 0: + while True: + if is_cell_walkable(wrld, ox - dx, oy + dy) \ + and not is_cell_walkable(wrld, ox + dx, oy - dy) \ + or is_cell_walkable(wrld, ox + dx, oy - dy) \ + and not is_cell_walkable(wrld, ox - dx, oy + dy): + return ox, oy + if jump(original, dx, 0, wrld, goal) or jump(original, 0, dy, wrld, goal): + return ox, oy + + ox += dx + oy += dy + + if not is_cell_walkable(wrld, ox, oy): + return None + if (ox, oy) == goal: + return (ox, oy) + else: + # Moving in y-axis + if dx != 0: + while True: + if is_cell_walkable(wrld, ox + dx, ny + 1) \ + and not is_cell_walkable(wrld, ox, ny + 1) \ + or is_cell_walkable(wrld, ox + dx, ny - 1) \ + and not is_cell_walkable(wrld, ox, ny - 1): + return ox, ny + ox += dx + if (ox, ny) == goal: + return (ox, ny) + if not is_cell_walkable(wrld, ox, ny): + return None + # Moving in x-axis + else: + while True: + if is_cell_walkable(wrld, nx + 1, oy + dy) \ + and not is_cell_walkable(wrld, nx + 1, oy) \ + or is_cell_walkable(wrld, nx - 1, oy + dy) \ + and not is_cell_walkable(wrld, nx - 1, oy): + return nx, oy + oy += dy + if (nx, oy) == goal: + return (nx, oy) + if not is_cell_walkable(wrld, nx, oy): + return None + return jump(current, dx, dy, wrld, goal) + + +def checkinsamedirection(prevmove, current, neighbor): + if abs(prevmove[0] - current[0]) == abs(current[0] - neighbor[0]): + return True + elif abs(prevmove[1] - current[1]) == abs(current[1] - neighbor[1]): + return True + else: + return False + + +def a_star2(wrld, goal=None, start=None): + # Original astar + found = False + if start is None: + start = character_location(wrld) # Start at current position + if goal is None: + goal = wrld.exitcell # Goal is exit cell + + cost_so_far = {start: 0} # Dictionary of costs to get to each cell + came_from = {start: None} # Dictionary of where each cell came from + + frontier = PriorityQueue() # Priority queue of cells to visit + frontier.put(start, 0) + + while not frontier.empty(): + current = frontier.get() + + if current == goal: + found = True + break + + # Check all walkable neighbors of current cell + for neighbor in eight_neighbors(wrld, current[0], current[1]): + # Calculate cost to get to neighbor - 1 or 1.4 + new_cost = cost_so_far[current] + euclidean_dist(current, neighbor) + + # If neighbor has no path or new path is better, update path + if neighbor not in cost_so_far or new_cost < cost_so_far[neighbor]: + cost_so_far[neighbor] = new_cost + priority = new_cost + euclidean_dist(neighbor, goal) + frontier.put(neighbor, priority) + came_from[neighbor] = current + + if not found: + return [(start), (start)] + + # Reconstruct path using came_from dictionary + currPos = goal + finalPath = [] + finalPath.append(goal) + while currPos != start: + currPos = came_from[currPos] + finalPath.append(currPos) + finalPath.reverse() + return finalPath + + +def character_location(wrld): + """Returns the location of the character in wrld. + wrld: World object + returns: (x, y) tuple""" + if len(wrld.characters) == 0: + Exception("No character in world") + return next(iter(wrld.characters.items()))[1][0].x, next(iter(wrld.characters.items()))[1][0].y + + +def exit_location(wrld): + """Returns the location of the exit in wrld. + wrld: World object + returns: (x, y) tuple""" + return wrld.exitcell[0], wrld.exitcell[1] + + +def monster_location(wrld): + """Returns the location of the nearest monster in wrld. + wrld: World object + returns: (x, y) tuple""" + if len(wrld.monsters) == 0: + print("No monster in world") + return 1, 0 + realMonsters = [] + monsters = list(wrld.monsters.values()) + for monster in monsters: + realMonsters.append(monster[0]) + monsters = realMonsters + monsters.sort(key=lambda m: euclidean_dist(character_location(wrld), (m.x, m.y))) + return monsters[0].x, monsters[0].y + + +def manhattan_distance_to_exit(wrld, start=None): + """Returns the manhattan distance to the exit. + wrld: World object + start (optional): (x, y) tuple to start from, defaults to character location + returns: float""" + if start is None: + start = character_location(wrld) + + return abs(start[0] - wrld.exitcell[0]) + abs(start[1] - wrld.exitcell[1]) + + +def euclidean_distance_to_exit(wrld, start=None): + """Returns the euclidean distance to the exit. + wrld: World object + start (optional): (x, y) tuple to start from, defaults to character location + returns: float""" + if start is None: + start = character_location(wrld) + + return ((start[0] - wrld.exitcell[0]) ** 2 + (start[1] - wrld.exitcell[1]) ** 2) ** 0.5 + + +def manhattan_distance_to_monster(wrld, start=None): + """Returns the manhattan distance to the closest monster. + wrld: World object + start (optional): (x, y) tuple to start from, defaults to character location + returns: float""" + + if start is None: + start = character_location(wrld) + + if len(wrld.monsters) == 0: + return 999 + else: + return min( + [abs(start[0] - monster[1][0].x) + abs(start[1] - monster[1][0].y) for monster in wrld.monsters.items()]) + + +def euclidean_distance_to_monster(wrld, start=None): + """Returns the euclidean distance to the closest monster. + wrld: World object + start (optional): (x, y) tuple to start from, defaults to character location + returns: float""" + + if start is None: + start = character_location(wrld) + + if len(wrld.monsters) == 0: + return 999 + else: + return min([((start[0] - monster[1][0].x) ** 2 + (start[1] - monster[1][0].y) ** 2) ** 0.5 for monster in + wrld.monsters.items()]) + + +def a_star_distance_to_exit(wrld, start=None): + """Returns the a* distance to the exit. + wrld: World object + start (optional): (x, y) tuple to start from, defaults to character location + returns: float""" + if start is None: + return len(a_star(wrld, goal=wrld.exitcell)) + else: + path = a_star(wrld, goal=wrld.exitcell, start=start); + # print("astar is") + # print(path.pop(len(path)-1)) + if path.pop(len(path) - 1) != wrld.exitcell: + return -1 + else: + return len(path) + + +def a_star_distance_to_monster(wrld, start=None): + """Returns the a* distance to the closest monster. + wrld: World object + start (optional): (x, y) tuple to start from, defaults to character location + returns: float""" + + if start is None: + return len(a_star(wrld, goal=monster_location(wrld))) + else: + return len(a_star(wrld, goal=monster_location(wrld), start=start)) + + +def a_star_distance(wrld, start, goal): + """Returns the a* distance between two points. + wrld: World object + start: (x, y) tuple to start from + goal: (x, y) tuple to end at + returns: float""" + if not is_cell_walkable(wrld, goal[0], goal[1]): + print("Goal is not walkable!") + raise Exception("Goal is not walkable!", goal) + if not is_cell_walkable(wrld, start[0], start[1]): + print("Start is not walkable!") + raise Exception("Start is not walkable!", start) + + try: + return len(a_star(wrld, goal=goal, start=start)) + except: # When A* fails and returns None, return a large number + return 999 + + +def monster_travel_direction(wrld): + """Returns the direction the monster is moving in. + wrld: World object + returns: (x, y) tuple""" + if len(wrld.monsters) == 0: + Exception("No monster in world") + return next(iter(wrld.monsters.items()))[1][0].dx, next(iter(wrld.monsters.items()))[1][0].dy