diff --git a/README.md b/README.md index ddf3a4e..a2cfa95 100644 --- a/README.md +++ b/README.md @@ -27,28 +27,20 @@ Feel free to make a pull request if you have any improvements or create a issue ## Requirements & Dependencies -- Tesseract v5.0+ - Python 3.10+ ``` keyboard==0.13.5 mouse==0.7.1 -mss==6.1.0 -numpy==1.22.3 -opencv_python==4.5.5.64 -pytesseract==0.3.9 +mss==9.0.1 +numpy==1.25.1 +opencv_python==4.8.0.74 +Pymem==1.12.0 +pywin32==306 ``` ## Installation of dependencies: -The script relies on tesseract (tested with v5.3.0) which can be installed using this [this](https://github.com/UB-Mannheim/tesseract/wiki) guide. -(*If by any chance the tesseract installation directory is different from the directory specified in Bot.py you need to manually change that in the script. Otherwise the bot will not work!*) - -default path (all users tesseract installation): -```py -pytesseract.pytesseract.tesseract_cmd = r'C:\Program Files\Tesseract-OCR\tesseract.exe' -``` - The python library requirments can be installed using `python -m pip install -r requirements.txt` or by running `Install_Requirements.bat` diff --git a/Run.bat b/Run.bat index b949ad6..41e3194 100644 --- a/Run.bat +++ b/Run.bat @@ -1 +1 @@ -python src/main.py --gameplan_path "gameplans/Dark_Castle_Hard_Standard" --restart \ No newline at end of file +python src/main.py --gameplan_path "gameplans/Dark_Castle_Hard_Chimps" \ No newline at end of file diff --git a/gameplans/Dark_Castle_Hard_Chimps/notes.md b/gameplans/Dark_Castle_Hard_Chimps/notes.md index d17ae7d..3141e57 100644 --- a/gameplans/Dark_Castle_Hard_Chimps/notes.md +++ b/gameplans/Dark_Castle_Hard_Chimps/notes.md @@ -18,6 +18,6 @@ ...and 1 insta-monkey! -# Credits to : Tanttinen#4001 +# Credits to : mirko93s *Note: The strat that is being used has RNG because of the Alchemist, so the results may differ to you from what I got* \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 839e4f8..9de92be 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ keyboard==0.13.5 mouse==0.7.1 -mss==6.1.0 -numpy==1.22.3 -opencv_python==4.5.5.64 -pytesseract==0.3.9 +mss==9.0.1 +numpy==1.25.1 +opencv_python==4.8.0.74 +Pymem==1.12.0 +pywin32==306 diff --git a/src/Bot.py b/src/Bot.py index 2f61702..6f46aa1 100644 --- a/src/Bot.py +++ b/src/Bot.py @@ -9,12 +9,13 @@ from pathlib import Path # Local imports -import ocr import recognition import simulatedinput import monitor import gameplan from logger import logger as log +import pymem +from winreg import * # Definently fix this class Bot(): @@ -37,8 +38,6 @@ def __init__(self, # Something to do with how python handles copying objects self._game_plan_copy = copy.deepcopy(self.game_plan) #### - - self.round_area = None self.DEBUG = debug_mode self.RESTART = restart_mode @@ -308,7 +307,6 @@ def set_static_target(self, tower_position, target_pos): def remove_tower(self, position): simulatedinput.click(position) simulatedinput.send_key("backspace") - simulatedinput.send_key("esc") def execute_instruction(self, instruction): """Handles instructions from the gameplan""" @@ -401,33 +399,17 @@ def check_for_collection_crates(self): # Can this be done better? if self.checkFor("diamond_case"): log.debug("easter collection detected") - - simulatedinput.click("EASTER_COLLECTION") #DUE TO EASTER EVENT: - time.sleep(1) - simulatedinput.click("LEFT_INSTA") # unlock insta - time.sleep(1) - simulatedinput.click("LEFT_INSTA") # collect insta - time.sleep(1) - simulatedinput.click("RIGHT_INSTA") # unlock r insta - time.sleep(1) - simulatedinput.click("RIGHT_INSTA") # collect r insta - time.sleep(1) - simulatedinput.click("F_LEFT_INSTA") - time.sleep(1) - simulatedinput.click("F_LEFT_INSTA") - time.sleep(1) - simulatedinput.click("MID_INSTA") # unlock insta - time.sleep(1) - simulatedinput.click("MID_INSTA") # collect insta + #c lick collect button + simulatedinput.click("EVENT_COLLECTION", timeout=1.5) + # collect instas + simulatedinput.click("LEFT_INSTA", amount=2, timeout=1.5) + simulatedinput.click("RIGHT_INSTA", amount=2, timeout=1.5) + simulatedinput.click("F_LEFT_INSTA", amount=2, timeout=1.5) + simulatedinput.click("MID_INSTA", amount=2, timeout=1.5) + simulatedinput.click("F_RIGHT_INSTA", amount=2, timeout=1.5) + # click continue and exit to main menu time.sleep(1) - simulatedinput.click("F_RIGHT_INSTA") - time.sleep(1) - simulatedinput.click("F_RIGHT_INSTA") - time.sleep(1) - - time.sleep(1) - simulatedinput.click("EASTER_CONTINUE") - + simulatedinput.click("EVENT_CONTINUE",timeout=1) simulatedinput.send_key("esc") # select hero if not selected @@ -524,94 +506,23 @@ def wait_for_loading(self): still_loading = self.checkFor("loading_screen") log.debug("Out of loading screen, continuing..") - - def getRoundArea(self): - # Init round area dict with width and height of the round area - round_area = { - "width": 200, - "height": 42 - } - - # Search for round text, returns (1484,13) on 1080p - area = self.checkFor("round", return_cords=True, center_on_found=False) - log.debug("this should be only printed once, getting round area") - log.debug(f"Round area found at {area}, applying offsetts") - - if area: - log.info("Found round area!") - - # set round area to the found area + offset - x, y, roundwidth, roundheight = area - - # Fiddled offset, do not tuch - # Offset from ROUND text to round number - xOffset = roundwidth + 10 - yOffset = int(roundheight * 3) - 40 - - round_area["top"] = y + yOffset - round_area["left"] = x - xOffset - - return round_area - - # If it cant find anything - log.warning("Could not find round area, setting default values") - default_round_area_scaled = monitor.scaling([0.7083333333333333, 0.0277777777777778]) # Use default values, (1360,30) on 1080p - - # left = x, top = y - round_area["left"] = default_round_area_scaled[0] - round_area["top"] = default_round_area_scaled[1] - return round_area - + def getRound(self): - # If round area is not located yet - if self.round_area is None: - self.round_area = self.getRoundArea() - - # Setting up screen capture area - # The screen part to capture - screenshot_dimensions = { - 'top': self.round_area["top"], - 'left': self.round_area["left"], - 'width': self.round_area["width"], - 'height': self.round_area["height"] + 50 - } - - # Take Screenshot - with mss.mss() as screenshotter: - screenshot = screenshotter.grab(screenshot_dimensions) - found_text, _ocrImage = ocr.getTextFromImage(screenshot) - - if self.DEBUG: - from cv2 import imwrite, IMWRITE_PNG_COMPRESSION - def get_valid_filename(s): - s = str(s).strip().replace(' ', '_') - return re.sub(r'(?u)[^-\w.]', '', s) - imwrite(f"./DEBUG/OCR_DONE_FOUND_{get_valid_filename(found_text)}_{str(time.time())}.png", _ocrImage, [IMWRITE_PNG_COMPRESSION, 0]) - - # Get only the first number/group so we don't need to replace anything in the string - if re.search(r"(\d+/\d+)", found_text): - found_text = re.search(r"(\d+)", found_text) - return int(found_text.group(0)) - else: - # If the found text does not match the regex requirements, Debug and save image - log.warning("Found text '{}' does not match regex requirements".format(found_text)) - - try: - file_path = Path(__file__).resolve().parent.parent/ "DEBUG" - if not file_path.exists(): - Path.mkdir(file_path) + add = 0x03096928 + offsets = [0xB8, 0x0, 0xC0, 0x120, 0x20, 0x20] - with open(file_path/f"GETROUND_IMAGE_{str(time.time())}.png", "wb") as output_file: - output_file.write(mss.tools.to_png(screenshot.rgb, screenshot.size)) - - log.warning("Saved screenshot of what was found") + pm = pymem.Pymem('BloonsTD6.exe') + gameModule = pymem.process.module_from_name(pm.process_handle, "GameAssembly.dll").lpBaseOfDll + address = pm.read_longlong(gameModule+add) - except Exception as e: - log.error(e) - log.warning("Could not save screenshot of what was found") + for offset in offsets: + try: + address = pm.read_longlong(address + offset) + except Exception as e: + print(e) - return None + return address + offsets[-1] -32 def waitForRound(self, round) -> None: """Wait for a specific round to start, use this to wait until to execute a gameplan instruction""" @@ -660,6 +571,59 @@ def checkFor(self, return_cords, center_on_found ) + + def findStore(self): + # check if game is installed on Steam + reg = ConnectRegistry(None,HKEY_CURRENT_USER) + game_on_Steam = False + try: + key = OpenKey(reg, r"SOFTWARE\Valve\Steam\Apps\960090") + if key: + try: + isInstalled = QueryValueEx(key, "Installed")[0] + # Other useful values are "Running" and "Updating" + if isInstalled == 1: + game_on_Steam = True + print("Detected Steam installation.") + except WindowsError: + # game is not installed but in the Steam library ??? + pass + CloseKey(key) + except WindowsError: + # game is not installed on Steam + pass + CloseKey(reg) + + # if game is not installed on Steam check Epic Games + reg = ConnectRegistry(None,HKEY_LOCAL_MACHINE) + game_on_EpicGames = False + try: + key = OpenKey(reg, r"SOFTWARE\WOW6432Node\Epic Games\EpicGamesLauncher") + if key: + try: + epicgamesLauncherPath = QueryValueEx(key, "AppDataPath")[0] + if epicgamesLauncherPath: + path = re.search(r".+Epic\\", epicgamesLauncherPath).group(0) + "UnrealEngineLauncher\LauncherInstalled.dat" + try: + with open(path) as f: + data = json.load(f) + for x in data["InstallationList"]: + if x["NamespaceId"] == "6a8dfa6e441e4f2f9048a98776c6077d": + game_on_EpicGames = True + print("Detected Epic Games installation.") + except: + # game is not installed on Epic Games + pass + except WindowsError: + # couldn't find Epic Games launcher path + pass + CloseKey(key) + except WindowsError: + # Epic Games launcher is not installed + pass + CloseKey(reg) + + return(game_on_Steam,game_on_EpicGames) if __name__ == "__main__": import time diff --git a/src/assets/admiral_brickell_3.png b/src/assets/admiral_brickell_3.png new file mode 100644 index 0000000..d2782a0 Binary files /dev/null and b/src/assets/admiral_brickell_3.png differ diff --git a/src/assets/captain_churchill_3.png b/src/assets/captain_churchill_3.png new file mode 100644 index 0000000..bc9e9fb Binary files /dev/null and b/src/assets/captain_churchill_3.png differ diff --git a/src/assets/instamonkey.png b/src/assets/instamonkey.png index e49875c..feeccdb 100644 Binary files a/src/assets/instamonkey.png and b/src/assets/instamonkey.png differ diff --git a/src/assets/pat_fusty_3.png b/src/assets/pat_fusty_3.png new file mode 100644 index 0000000..4484d8c Binary files /dev/null and b/src/assets/pat_fusty_3.png differ diff --git a/src/assets/sauda_3.png b/src/assets/sauda_3.png new file mode 100644 index 0000000..f4bb19d Binary files /dev/null and b/src/assets/sauda_3.png differ diff --git a/src/assets/startup.png b/src/assets/startup.png new file mode 100644 index 0000000..c3fec89 Binary files /dev/null and b/src/assets/startup.png differ diff --git a/src/main.py b/src/main.py index 2a916c9..f1df38a 100644 --- a/src/main.py +++ b/src/main.py @@ -7,8 +7,11 @@ from Failsafe import FailSafe import mouse import simulatedinput +import monitor from logger import logger as log import os +import win32gui +import subprocess def main(arg_parser): @@ -60,15 +63,52 @@ def no_gameplan_exception(): print("Gamemode:", bot.settings["GAMEMODE"].replace("_", " ").title()) print("="*25) - print("Waiting for Home screen. Please switch to the Bloons TD 6 window.") + print("Finding game process.") - # Wait for btd6 home screen + # set mouse starting position to the bottom right corner + simulatedinput.move_mouse((monitor.width,monitor.height)) + + waiting_for_game = True + hwnd = win32gui.FindWindow(None, 'BloonsTD6') + + # game is not open + if not hwnd: + print("Game process not found. Finding game installations.") + + Steam, EpicGames = bot.findStore() + + if Steam: + print("Starting the game through Steam. Please wait...") + subprocess.run("start steam://run/960090", shell=True, check=True) + + elif EpicGames: + print("Starting the game through Epic Games. Please wait...") + subprocess.run("start com.epicgames.launcher://apps/6a8dfa6e441e4f2f9048a98776c6077d%3A49c4bf5c6fd24259b87d0bcc96b6009f%3A7786b355a13b47a6b3915335117cd0b2?action=launch", shell=True, check=True) + + else: + # We can exit the program here if we don't want cracked users to use the bot + # sys.exit(1) + print("Please start the game manually.") + + while waiting_for_game: + time.sleep(0.2) + hwnd = win32gui.FindWindow(None, 'BloonsTD6') + # game process found, focus its window + if hwnd: + waiting_for_game = False + win32gui.SetForegroundWindow(hwnd) + print("Game found. Switching to game window.") + + # Wait for btd6 home screen or startup screen waiting_for_home = False log.info("Waiting for home screen..") while waiting_for_home is False: time.sleep(0.2) # add a short timeout to avoid spamming the cpu - waiting_for_home = bot.checkFor("home_menu") + waiting_for_startup, waiting_for_home = bot.checkFor(["startup","home_menu"], return_raw=True) + if waiting_for_startup: + log.info("Startup screen detected") + simulatedinput.click("STARTUP") log.info("Home screen detected") print("Starting bot..\nIf you want to stop the bot, move your mouse to the upper left corner of your screen or press ctrl+c in the terminal") @@ -109,7 +149,7 @@ def no_gameplan_exception(): parser.add_argument('-p', '--path', '--gameplan_path', type=str, help='Path to the gameplan directory', required=True) parser.add_argument('-d', '--debug', action='store_true', help='Enable debug mode') - parser.add_argument('-r', '--restart', action='store_true', help='automatically restarts the game when finished, instead of going to home screen') + parser.add_argument('-r', '--restart', action='store_true', help='automatically restarts the game when finished, instead of going to home screen \(games don\'t count towards event progression if you don\'t go back to home)') parser.add_argument('-s', '--sandbox', action='store_true', help='Try put gameplan in sandbox mode without waiting for specific rounds') # Start the bot on a seperate thread diff --git a/src/ocr.py b/src/ocr.py deleted file mode 100644 index e25d4b4..0000000 --- a/src/ocr.py +++ /dev/null @@ -1,90 +0,0 @@ -# width, height, top, left - -import pytesseract - -import numpy as np -import cv2 -import sys -import time - -if sys.platform == "win32": - pytesseract.pytesseract.tesseract_cmd = r'C:\Program Files\Tesseract-OCR\tesseract.exe' - - -def formatImageOCR(originalScreenshot): - screenshot = np.array(originalScreenshot, dtype=np.uint8) - # Get local maximum: - kernelSize = 5 - maxKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (kernelSize, kernelSize)) - localMax = cv2.morphologyEx(screenshot, cv2.MORPH_CLOSE, maxKernel, None, None, 1, cv2.BORDER_REFLECT101) - # Perform gain division - # print(screenshot, localMax) - gainDivision = np.where(localMax == 0, 0, (screenshot / localMax)) - # Clip the values to [0,255] - gainDivision = np.clip((255 * gainDivision), 0, 255) - # Convert the mat type from float to uint8: - gainDivision = gainDivision.astype("uint8") - # Convert RGB to grayscale: - grayscaleImage = cv2.cvtColor(gainDivision, cv2.COLOR_BGR2GRAY) - # Resize image to improve the quality - grayscaleImage = cv2.resize(grayscaleImage,(0,0), fx=3.0, fy=3.0) - # Get binary image via Otsu: - _, final_image = cv2.threshold(grayscaleImage, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU) - # cv2.imwrite(f"./DEBUG/OCR_FORMAT_BINARY_IMAGE_{str(time.time())}.png", final_image, [cv2.IMWRITE_PNG_COMPRESSION, 0]) - - # Set kernel (structuring element) size: - kernelSize = 3 - # Set morph operation iterations: - opIterations = 1 - # Get the structuring element: - morphKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (kernelSize, kernelSize)) - # Perform closing: - final_image = cv2.morphologyEx( final_image, cv2.MORPH_CLOSE, morphKernel, None, None, opIterations, cv2.BORDER_REFLECT101 ) - # cv2.imwrite(f"./DEBUG/OCR_FORMAT_BEFORE_FLOOD_{str(time.time())}.png", final_image, [cv2.IMWRITE_PNG_COMPRESSION, 0]) - - # Flood fill (white + black): - cv2.floodFill(final_image, mask=None, seedPoint=(int(0), int(0)), newVal=(255)) - # Invert image so target blobs are colored in white: - final_image = 255 - final_image - # Find the blobs on the binary image: - contours, hierarchy = cv2.findContours(final_image, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) - # Process the contours: - for i, c in enumerate(contours): - # Get contour hierarchy: - currentHierarchy = hierarchy[0][i][3] - # Look only for children contours (the holes): - if currentHierarchy != -1: - # Get the contour bounding rectangle: - boundRect = cv2.boundingRect(c) - # Get the dimensions of the bounding rect: - rectX = boundRect[0] - rectY = boundRect[1] - rectWidth = boundRect[2] - rectHeight = boundRect[3] - # Get the center of the contour the will act as - # seed point to the Flood-Filling: - fx = rectX + 0.5 * rectWidth - fy = rectY + 0.5 * rectHeight - # Fill the hole: - cv2.floodFill(final_image, mask=None, seedPoint=(int(fx), int(fy)), newVal=(0)) - # cv2.imwrite(f"./DEBUG/OCR_FLOOD_{i}_{str(time.time())}.png", final_image, [cv2.IMWRITE_PNG_COMPRESSION, 0]) - - return final_image - - -# Change to https://stackoverflow.com/questions/66334737/pytesseract-is-very-slow-for-real-time-ocr-any-way-to-optimise-my-code -# or https://www.reddit.com/r/learnpython/comments/kt5zzw/how_to_speed_up_pytesseract_ocr_processing/ - -def getTextFromImage(image): - """ returns text from image """ - imageCandidate = formatImageOCR(image) - # Write result to disk: - - # DEBUG log round to disk - # import time - # cv2.imwrite(f"./DEBUG/{str(time.time())}.png", imageCandidate, [cv2.IMWRITE_PNG_COMPRESSION, 0]) - - # NOTE: This part seems to be buggy - # Get current round from screenshot with tesseract - return pytesseract.image_to_string(imageCandidate, config='--psm 7').replace("\n", ""), imageCandidate - diff --git a/src/simulatedinput.py b/src/simulatedinput.py index 0acac76..c68be85 100644 --- a/src/simulatedinput.py +++ b/src/simulatedinput.py @@ -58,5 +58,11 @@ def click(location: tuple | tuple, amount=1, timeout=0.5, move_timeout=0.1, hold """ if amount > 1: sleep(timeout) + + # keep this if or it will wait twice (double the time) if amount is 2+ + if amount == 1: + sleep(timeout) + + # Move mouse on the bottom right corner so we don't cover the screen with the cursor + move_mouse((monitor.width,monitor.height)) - sleep(timeout) \ No newline at end of file diff --git a/src/static.py b/src/static.py index 8d3fbe0..f166ccd 100644 --- a/src/static.py +++ b/src/static.py @@ -49,14 +49,14 @@ class Map (Difficulty, Gamemode): "VICTORY_HOME" : [ 0.366796875 , 0.7805555555555556 ], "DEFEAT_HOME" : [ 0.29453125 , 0.7520833333333333 ], "DEFEAT_HOME_NO_CONTINUE" : [ 0.36328125 , 0.7520833333333333 ], - "EASTER_COLLECTION" : [ 0.499609375 , 0.6326388888888889 ], + "EVENT_COLLECTION" : [ 0.499609375 , 0.6326388888888889 ], "F_LEFT_INSTA" : [ 0.3390625 , 0.5013888888888889 ], "F_RIGHT_INSTA" : [ 0.65625 , 0.5013888888888889 ], "LEFT_INSTA" : [ 0.41953125 , 0.5034722222222222 ], "RIGHT_INSTA" : [ 0.577734375 , 0.5027777777777778 ], "MID_INSTA" : [ 0.4984375 , 0.5048611111111111 ], - "EASTER_CONTINUE" : [ 0.5 , 0.9236111111111112 ], - "EASTER_EXIT" : [ 0.0390625 , 0.06458333333333334 ], + "EVENT_CONTINUE" : [ 0.5 , 0.9236111111111112 ], + "EVENT_EXIT" : [ 0.0390625 , 0.06458333333333334 ], "QUIT_HOME" : [ 0.43984375 , 0.7881944444444444 ], "HERO_SELECT" : [ 0.312109375 , 0.8833333333333333 ], "CONFIRM_HERO" : [ 0.587890625 , 0.5722222222222222 ], @@ -72,7 +72,8 @@ class Map (Difficulty, Gamemode): "CONFIRM_CHIMPS" : [ 0.500390625 , 0.6805555555555556 ], "SETTINGS" : [ 0.037500 , 0.191667], "LANGUAGE" : [ 0.555208 , 0.659259], - "ENGLISH" : [ 0.224479 , 0.192593] + "ENGLISH" : [ 0.224479 , 0.192593], + "STARTUP" : [ 0.5, 0.925] } hero_positions = { @@ -138,69 +139,79 @@ class Map (Difficulty, Gamemode): # "NAME" : [PAGE, INDEX] maps = { - "MONKEY_MEADOW" : [1, 1], - "TREE_STUMP" : [1, 2], - "TOWN_CENTER" : [1, 3], - "SCRAPYARD" : [1, 4], - "THE_CABIN" : [1, 5], - "RESORT" : [1, 6], - "SKATES" : [2, 1], - "LOTUS_ISLAND" : [2, 2], - "CANDY_FALLS" : [2, 3], - "WINTER_PARK" : [2, 4], - "CARVED" : [2, 5], - "PARK_PATH" : [2, 6], - "ALPINE_RUN" : [3, 1], - "FROZEN_OVER" : [3, 2], - "IN_THE_LOOP" : [3, 3], - "CUBISM" : [3, 4], - "FOUR_CIRCLES" : [3, 5], - "HEDGE" : [3, 6], - "END_OF_THE_ROAD" : [4, 1], - "LOGS" : [4, 2], - "COVERED_GARDEN" : [5, 1], - "QUARRY" : [5, 2], - "QUIET_STREET" : [5, 3], - "BLOONARIUS_PRIME" : [5, 4], - "BALANCE" : [5, 5], - "ENCRYPTED" : [5, 6], - "BAZAAR" : [6, 1], - "ADORAS_TEMPLE" : [6, 2], - "SPRING_SPRING" : [6, 3], - "KARTSNDARTS" : [6, 4], - "MOON_LANDING" : [6, 5], - "HAUNTED" : [6, 6], - "DOWNSTREAM" : [7, 1], - "FIRING_RANGE" : [7, 2], - "CRACKED" : [7, 3], - "STREAMBED" : [7, 4], - "CHUTES" : [7, 5], - "RAKE" : [7, 6], - "SPICE_ISLANDS" : [8, 1], - "MIDNIGHT_MANSION" : [9, 1], - "SUNKEN_COLUMNS" : [9, 2], - "XFACTOR" : [9, 3], - "MESA" : [9, 4], - "GEARED" : [9, 5], - "SPILLWAY" : [9, 6], - "CARGO" : [10, 1], - "PATS_POND" : [10, 2], - "PENINSULA" : [10, 3], - "HIGH_FINANCE" : [10, 4], - "ANOTHER_BRICK" : [10, 5], - "OFF_THE_COAST" : [10, 6], - "CORNFIELD" : [11, 1], - "UNDERGROUND" : [11, 2], - "SANCTUARY" : [12, 1], - "RAVINE" : [12, 2], - "FLOODED_VALLEY" : [12, 3], - "INFERNAL" : [12, 4], - "BLOODY_PUDDLES" : [12, 5], - "WORKSHOP" : [12, 6], - "QUAD" : [13, 1], - "DARK_CASTLE" : [13, 2], - "MUDDY_PUDDLES" : [13, 3], - "OUCH" : [13, 4] + # BEGINNER + "MONKEY_MEADOW" : [1, 1], + "TREE_STUMP" : [1, 2], + "TOWN_CENTER" : [1, 3], + "MIDDLE_OF_THE_ROAD": [1, 4], + "ONE_TWO_THREE" : [1, 5], + "SCRAPYARD" : [1, 6], + "THE_CABIN" : [2, 1], + "RESORT" : [2, 2], + "SKATES" : [2, 3], + "LOTUS_ISLAND" : [2, 4], + "CANDY_FALLS" : [2, 5], + "WINTER_PARK" : [2, 6], + "CARVED" : [3, 1], + "PARK_PATH" : [3, 2], + "ALPINE_RUN" : [3, 3], + "FROZEN_OVER" : [3, 4], + "IN_THE_LOOP" : [3, 5], + "CUBISM" : [3, 6], + "FOUR_CIRCLES" : [4, 1], + "HEDGE" : [4, 2], + "END_OF_THE_ROAD" : [4, 3], + "LOGS" : [4, 4], + # INTERMEDIATE + "WATER_PARK" : [5, 1], + "POLYPHEMUS" : [5, 2], + "COVERED_GARDEN" : [5, 3], + "QUARRY" : [5, 4], + "QUIET_STREET" : [5, 5], + "BLOONARIUS_PRIME" : [5, 6], + "BALANCE" : [6, 1], + "ENCRYPTED" : [6, 2], + "BAZAAR" : [6, 3], + "ADORAS_TEMPLE" : [6, 4], + "SPRING_SPRING" : [6, 5], + "KARTSNDARTS" : [6, 6], + "MOON_LANDING" : [7, 1], + "HAUNTED" : [7, 2], + "DOWNSTREAM" : [7, 3], + "FIRING_RANGE" : [7, 4], + "CRACKED" : [7, 5], + "STREAMBED" : [7, 6], + "CHUTES" : [8, 1], + "RAKE" : [8, 2], + "SPICE_ISLANDS" : [8, 3], + # ADVANCED + "EROSION" : [9, 1], + "MIDNIGHT_MANSION" : [9, 2], + "SUNKEN_COLUMNS" : [9, 3], + "X_FACTOR" : [9, 4], + "MESA" : [9, 5], + "GEARED" : [9, 6], + "SPILLWAY" : [10, 1], + "CARGO" : [10, 2], + "PATS_POND" : [10, 3], + "PENINSULA" : [10, 4], + "HIGH_FINANCE" : [10, 5], + "ANOTHER_BRICK" : [10, 6], + "OFF_THE_COAST" : [11, 1], + "CORNFIELD" : [11, 2], + "UNDERGROUND" : [11, 3], + # EXPERT + "DARK_DUNGEONS" : [12, 1], + "SANCTUARY" : [12, 2], + "RAVINE" : [12, 3], + "FLOODED_VALLEY" : [12, 4], + "INFERNAL" : [12, 5], + "BLOODY_PUDDLES" : [12, 6], + "WORKSHOP" : [13, 1], + "QUAD" : [13, 2], + "DARK_CASTLE" : [13, 3], + "MUDDY_PUDDLES" : [13, 4], + "OUCH" : [13, 5] } upgrade_keybinds = { @@ -211,4 +222,4 @@ class Map (Difficulty, Gamemode): # Index, regular targets, spike factory targets target_order_regular = [ "FIRST", "LAST", "CLOSE", "STRONG" ] -target_order_spike = [ "NORMAL", "CLOSE", "FAR", "SMART" ] \ No newline at end of file +target_order_spike = [ "NORMAL", "CLOSE", "FAR", "SMART" ]