forked from mirror/Archipelago
Some checks failed
Analyze modified files / flake8 (push) Failing after 2m28s
Build / build-win (push) Has been cancelled
Build / build-ubuntu2204 (push) Has been cancelled
ctest / Test C++ ubuntu-latest (push) Has been cancelled
ctest / Test C++ windows-latest (push) Has been cancelled
Analyze modified files / mypy (push) Has been cancelled
Build and Publish Docker Images / Push Docker image to Docker Hub (push) Successful in 5m4s
Native Code Static Analysis / scan-build (push) Failing after 5m2s
type check / pyright (push) Successful in 1m7s
unittests / Test Python 3.11.2 ubuntu-latest (push) Failing after 16m23s
unittests / Test Python 3.12 ubuntu-latest (push) Failing after 28m19s
unittests / Test Python 3.13 ubuntu-latest (push) Failing after 14m49s
unittests / Test hosting with 3.13 on ubuntu-latest (push) Successful in 5m0s
unittests / Test Python 3.13 macos-latest (push) Has been cancelled
unittests / Test Python 3.11 windows-latest (push) Has been cancelled
unittests / Test Python 3.13 windows-latest (push) Has been cancelled
371 lines
18 KiB
Python
371 lines
18 KiB
Python
from BaseClasses import Item, Location
|
|
from .Locations import grinch_locations_to_id, grinch_locations, GrinchLocation, get_location_names_per_category, GrinchLocationData
|
|
from .Items import (grinch_items_to_id, GrinchItem, ALL_ITEMS_TABLE, MISC_ITEMS_TABLE, get_item_names_per_category,
|
|
TRAPS_TABLE, MOVES_TABLE, USEFUL_ITEMS_TABLE)
|
|
from .Regions import connect_regions
|
|
from .Rules import set_location_rules
|
|
|
|
from .Client import *
|
|
from typing import ClassVar
|
|
|
|
from worlds.AutoWorld import World
|
|
from Options import OptionError
|
|
|
|
from .GrinchOptions import GrinchOptions
|
|
from .Web import GrinchWeb
|
|
|
|
|
|
class GrinchWorld(World):
|
|
game: ClassVar[str] = "The Grinch"
|
|
options_dataclass = GrinchOptions
|
|
options: GrinchOptions
|
|
topology_present = True # not an open world game, very linear, allows "Paths" in spoiler log
|
|
item_name_to_id: ClassVar[dict[str, int]] = grinch_items_to_id()
|
|
location_name_to_id: ClassVar[dict[str, int]] = grinch_locations_to_id()
|
|
required_client_version = (0, 6, 6) # Unused atm, replaced by ap.json
|
|
item_name_groups = get_item_names_per_category()
|
|
location_name_groups = get_location_names_per_category()
|
|
web = GrinchWeb()
|
|
|
|
songs_chosen: dict
|
|
|
|
ut_can_gen_without_yaml = True # class var that tells it to ignore the player YAML
|
|
|
|
def __init__(self, *args, **kwargs): # Pulls __init__ function and takes control from there in BaseClasses.py
|
|
self.origin_region_name: str = "Mount Crumpit"
|
|
super(GrinchWorld, self).__init__(*args, **kwargs)
|
|
self.songs_chosen = {}
|
|
|
|
def generate_early(self) -> None: # Special conditions changed before generation occurs
|
|
from CommonClient import logger
|
|
if self.options.ring_link == 1 and self.options.unlimited_eggs == 1:
|
|
raise OptionError("Cannot enable both unlimited rotten eggs and ring links. You can only enable one of " +
|
|
f"these at a time. The following player's YAML needs to be fixed: {self.player_name}")
|
|
|
|
# Total available weight sum of filler items.
|
|
# If this is 0, it means no filler was provided by the user, which will cause generation errors as there will
|
|
# be not enough items for all defined locations. Later this can be changed to default item and this get removed.
|
|
total_fillerweights = sum(self.options.filler_weight[filler] for filler in self.options.filler_weight.keys())
|
|
if total_fillerweights <= 0:
|
|
logger.warning(f"Player {self.player_name} has all filler weights set to 0. Using Presents instead for filler.")
|
|
|
|
total_trapweights = sum(self.options.trap_weight[trap] for trap in self.options.trap_weight.keys())
|
|
if total_trapweights <= 0 and self.options.trap_percentage >= 1:
|
|
raise OptionError("Cannot begin generation as no trap options are defined. At least one trap item " +
|
|
f"must have a weight of at least 1. The following player's YAML needs to be fixed: {self.player_name}")
|
|
|
|
if self.options.music_rando.value == 1:
|
|
for music_enabled_region, region_data in ALL_REGIONS_INFO.items():
|
|
if region_data.allow_music_rando:
|
|
self.songs_chosen[music_enabled_region] = self.random.randint(2, 22)
|
|
|
|
# this handles all related logical UT things
|
|
if hasattr(self.multiworld, "re_gen_passthrough"):
|
|
if self.game in self.multiworld.re_gen_passthrough:
|
|
slot_data = self.multiworld.re_gen_passthrough[self.game]
|
|
print(slot_data)
|
|
self.options.unlimited_eggs.value = slot_data["unlimited_eggs"]
|
|
self.options.starting_area.value = slot_data["starting_area"]
|
|
self.options.exclude_environments.value = ["exclude_environments"]
|
|
self.options.giftsanity.value = slot_data["giftsanity"]
|
|
self.options.progressive_vacuums = slot_data["progressive_vacuums"]
|
|
self.options.missionsanity = slot_data["missionsanity"]
|
|
self.options.supadow_minigames = slot_data["supadow_minigames"]
|
|
self.options.move_rando = slot_data["move_rando"]
|
|
self.options.moves_to_randomize = slot_data["moves_to_randomize"]
|
|
self.options.gadget_rando = slot_data["gadget_rando"]
|
|
self.options.gadgets_to_randomize = slot_data["gadgets_to_randomize"]
|
|
self.options.exclude_gc = slot_data["exclude_gc"]
|
|
self.options.progressive_gadgets = slot_data["progressive_gadgets"]
|
|
self.options.killsanity = slot_data["killsanity"]
|
|
self.options.misc_checks = slot_data["misc_checks"]
|
|
|
|
def create_regions(self): # Generates all regions for the multiworld
|
|
connect_regions(self, self.multiworld)
|
|
|
|
wv_subareas: set[str] = {
|
|
"Post Office",
|
|
"Clock Tower",
|
|
"City Hall",
|
|
}
|
|
wf_subareas: set[str] = {
|
|
"Civic Center",
|
|
"Ski Resort",
|
|
}
|
|
wd_subareas: set[str] = {
|
|
"Minefield",
|
|
"Power Plant",
|
|
"Generator Building",
|
|
}
|
|
wl_subareas: set[str] = {
|
|
"Scout's Hut",
|
|
"North Shore",
|
|
"Mayor's Villa",
|
|
"Submarine World",
|
|
}
|
|
|
|
for location, data in grinch_locations.items():
|
|
region = self.get_region(data.region)
|
|
|
|
if location == "MC - Sleigh Ride - Neutralizing Santa":
|
|
region.add_event(location, "Goal", None, Location, Item)
|
|
continue
|
|
|
|
# No .value after self.options because UT no likey
|
|
if location == "MC - Unlock the Grinch Copter" and self.options.exclude_gc:
|
|
continue
|
|
|
|
# No .value after self.options because UT no likey
|
|
if "Giftsanity" in data.location_group and (not self.options.giftsanity or self.options.exclude_gc):
|
|
continue
|
|
|
|
# No .value after self.options because UT no likey
|
|
if "Missions" in data.location_group and self.options.missionsanity in [0,2]:
|
|
continue
|
|
|
|
# No .value after self.options because UT no likey
|
|
if "Missionsanity" in data.location_group and self.options.missionsanity in [0,1]:
|
|
continue
|
|
|
|
if "Miscellaneous" in data.location_group and self.options.misc_checks == False:
|
|
continue
|
|
|
|
if location == "WV - Squashing All Gifts":
|
|
exclude_wv_squash: bool = False
|
|
for wv_sub in wv_subareas:
|
|
if wv_sub in self.options.exclude_environments.value:
|
|
exclude_wv_squash = True
|
|
|
|
if exclude_wv_squash:
|
|
continue # Ignores the creation of WV Squashing all Gifts
|
|
|
|
elif location == "WF - Squashing All Gifts":
|
|
exclude_wf_squash: bool = False
|
|
for wf_sub in wf_subareas:
|
|
if wf_sub in self.options.exclude_environments.value:
|
|
exclude_wf_squash = True
|
|
|
|
if exclude_wf_squash:
|
|
continue # Ignores the creation of WF Squashing all Gifts
|
|
|
|
elif location == "WD - Squashing All Gifts":
|
|
exclude_wd_squash: bool = False
|
|
for wd_sub in wd_subareas:
|
|
if wd_sub in self.options.exclude_environments.value:
|
|
exclude_wd_squash = True
|
|
|
|
if exclude_wd_squash:
|
|
continue # Ignores the creation of WD Squashing all Gifts
|
|
|
|
elif location == "WL - Squashing All Gifts":
|
|
exclude_wl_squash: bool = False
|
|
for wl_sub in wl_subareas:
|
|
if wl_sub in self.options.exclude_environments.value:
|
|
exclude_wl_squash = True
|
|
|
|
if exclude_wl_squash:
|
|
continue # Ignores the creation of WL Squashing all Gifts
|
|
|
|
# If the region is in the list to be ignored, DON'T create the location and just continue.
|
|
# Ex if Mount Crumpit is in the exclude env list, no locations should exist in Mount Crumpit.
|
|
if region.name in self.options.exclude_environments.value:
|
|
if region.name == "Mount Crumpit":
|
|
logger.warning(f"Player {self.player_name} has excluded Mount Crumpit, which is where a large number of Sphere 1 locations usually exist.")
|
|
continue
|
|
|
|
|
|
entry = GrinchLocation(self.player, location, region, data)
|
|
region.locations.append(entry)
|
|
|
|
def create_item(self, item: str) -> GrinchItem: # Creates specific items on demand
|
|
if item in ALL_ITEMS_TABLE.keys():
|
|
return GrinchItem(item, self.player, ALL_ITEMS_TABLE[item])
|
|
|
|
raise Exception(f"Invalid item name: {item}")
|
|
|
|
def create_items(self): # Generates all items for the multiworld
|
|
self_itempool: list[GrinchItem] = []
|
|
sub_area_items: dict[str, list[str]] = {
|
|
"Who Cloak": ["Post Office"],
|
|
"Scout Clothes": ["Mayor's Villa", "North Shore"],
|
|
"Cable Car Access Card": ["Ski Resort"],
|
|
}
|
|
missionsanity_items: dict[str, list[str]] = {
|
|
"Who Cloak": ["Post Office"],
|
|
"Scout Clothes": ["Mayor's Villa", "North Shore"],
|
|
"Drill": ["North Shore"],
|
|
"Painting Bucket": ["Whoville"],
|
|
}
|
|
|
|
# Precollected items is stored per player. First, we must get the current player's starting inventory.
|
|
# From here, we get an AP item list. But, we only care about the name. So we get a list of strings as a result.
|
|
player_start_inv: list[str] = [item.name for item in self.multiworld.precollected_items[self.player]]
|
|
|
|
for item, data in {**SLEIGH_TABLE}.items():
|
|
# Only create the item if it doesn't already exist in the player's start inventory.
|
|
if not item in player_start_inv:
|
|
self_itempool.append(self.create_item(item))
|
|
|
|
for hearts_added in USEFUL_ITEMS_TABLE:
|
|
if hearts_added == grinch_items.useful_items.HEART_OF_STONE:
|
|
# Get the count of already created Heart of Stone items, but capped to 4
|
|
heart_stone_count: int = min(player_start_inv.count(grinch_items.useful_items.HEART_OF_STONE), 4)
|
|
for _ in range(4 - heart_stone_count):
|
|
self_itempool.append(self.create_item(hearts_added))
|
|
|
|
for mission_item in MISSION_ITEMS_TABLE:
|
|
# Only create the item if it doesn't already exist in the player's start inventory.
|
|
if mission_item in player_start_inv:
|
|
continue
|
|
|
|
# Checks to see if there are any locations in the Sub-area list.
|
|
sub_area_has_no_locations: bool = False
|
|
|
|
if mission_item in sub_area_items:
|
|
sub_area_has_no_locations = True
|
|
for grinch_reg in sub_area_items[mission_item]:
|
|
if len(self.get_region(grinch_reg).get_locations()) > 0:
|
|
sub_area_has_no_locations = False
|
|
|
|
# If the item is a sub_area_item that has 0 locations, add it to start inventory
|
|
if sub_area_has_no_locations:
|
|
self.multiworld.push_precollected(self.create_item(mission_item))
|
|
player_start_inv.append(mission_item)
|
|
# Else if the player disables missionsanity, add the item into start inventory
|
|
# No .value after self.options.missionsanity because UT no likey
|
|
elif self.options.missionsanity == 0:
|
|
self.multiworld.push_precollected(self.create_item(mission_item))
|
|
player_start_inv.append(mission_item)
|
|
elif self.options.missionsanity == 2:
|
|
if mission_item in missionsanity_items:
|
|
self_itempool.append(self.create_item(mission_item))
|
|
else:
|
|
self.multiworld.push_precollected(self.create_item(mission_item))
|
|
player_start_inv.append(mission_item)
|
|
# Else, let the multiworld create the item normally.
|
|
else:
|
|
self_itempool.append(self.create_item(mission_item))
|
|
|
|
# Add various moves that the user requested.
|
|
for moves_added in MOVES_TABLE:
|
|
# Only create the item if it doesn't already exist in the player's start inventory.
|
|
if moves_added in player_start_inv:
|
|
continue
|
|
|
|
if self.options.move_rando and moves_added in self.options.moves_to_randomize:
|
|
self_itempool.append(self.create_item(moves_added))
|
|
else:
|
|
self.multiworld.push_precollected(self.create_item(moves_added))
|
|
player_start_inv.append(moves_added)
|
|
|
|
# Adds gadgets
|
|
for gadgets_added in GADGETS_TABLE:
|
|
if gadgets_added == "Grinch Copter" and self.options.exclude_gc:
|
|
continue
|
|
|
|
if gadgets_added == "Marine Mobile" and "Submarine World" in self.options.exclude_environments:
|
|
self.multiworld.push_precollected(self.create_item(gadgets_added))
|
|
player_start_inv.append(gadgets_added)
|
|
continue
|
|
|
|
# Only create the item if it doesn't already exist in the player's start inventory.
|
|
elif gadgets_added in player_start_inv:
|
|
continue
|
|
|
|
if self.options.gadget_rando and gadgets_added in self.options.gadgets_to_randomize:
|
|
self_itempool.append(self.create_item(gadgets_added))
|
|
else:
|
|
self.multiworld.push_precollected(self.create_item(gadgets_added))
|
|
player_start_inv.append(gadgets_added)
|
|
continue
|
|
|
|
if not self.options.progressive_vacuums:
|
|
# When the starting area is chosen, add the key to the starting inventory.
|
|
if self.options.starting_area.value == 0:
|
|
self.multiworld.push_precollected(self.create_item("Whoville Vacuum Tube"))
|
|
player_start_inv.append("Whoville Vacuum Tube")
|
|
elif self.options.starting_area.value == 1:
|
|
self.multiworld.push_precollected(self.create_item("Who Forest Vacuum Tube"))
|
|
player_start_inv.append("Who Forest Vacuum Tube")
|
|
elif self.options.starting_area.value == 2:
|
|
self.multiworld.push_precollected(self.create_item("Who Dump Vacuum Tube"))
|
|
player_start_inv.append("Who Dump Vacuum Tube")
|
|
elif self.options.starting_area.value == 3:
|
|
self.multiworld.push_precollected((self.create_item("Who Lake Vacuum Tube")))
|
|
player_start_inv.append("Who Lake Vacuum Tube")
|
|
else:
|
|
self.multiworld.push_precollected((self.create_item("Progressive Vacuum Tube")))
|
|
player_start_inv.append("Progressive Vacuum Tube")
|
|
|
|
if not self.options.progressive_vacuums:
|
|
for vacuums_added in KEYS_TABLE.keys():
|
|
if vacuums_added == "Progressive Vacuum Tube":
|
|
continue
|
|
|
|
if vacuums_added not in player_start_inv:
|
|
self_itempool.append(self.create_item(vacuums_added))
|
|
else:
|
|
progress_vac_count: int = min(player_start_inv.count("Progressive Vacuum Tube"),4)
|
|
for _ in range(4 - progress_vac_count):
|
|
self_itempool.append(self.create_item("Progressive Vacuum Tube"))
|
|
|
|
# Get number of current unfilled locations
|
|
unfilled_locations: int = len(self.multiworld.get_unfilled_locations(self.player)) - len(self_itempool)
|
|
trap_locations: int = int(math.floor(unfilled_locations * (self.options.trap_percentage / 100)))
|
|
filler_locations = unfilled_locations - trap_locations
|
|
|
|
# If trap_locations is 0, this will automatically get skipped
|
|
for _ in range(trap_locations):
|
|
# Keys are the individual items, values are the weights based on the option being set
|
|
self_itempool.append(self.create_item(self.get_weighted_filler_item
|
|
(list(self.options.trap_weight.keys()), list(self.options.trap_weight.values()))))
|
|
|
|
total_fillerweights = sum(self.options.filler_weight[filler] for filler in self.options.filler_weight.keys())
|
|
for _ in range(filler_locations):
|
|
if total_fillerweights > 0:
|
|
# Keys are the individual items, values are the weights based on the option being set
|
|
self_itempool.append(self.create_item(self.get_weighted_filler_item(
|
|
list(self.options.filler_weight.keys()), list(self.options.filler_weight.values()))))
|
|
else:
|
|
self_itempool.append(self.create_item("Present"))
|
|
|
|
self.multiworld.itempool += self_itempool
|
|
|
|
def set_rules(self):
|
|
self.multiworld.completion_condition[self.player] = lambda state: state.has("Goal", self.player)
|
|
set_location_rules(self)
|
|
|
|
def get_weighted_filler_item(self, other_filler: list[str], weights_dict: list[int]) -> str:
|
|
# The below does this for deterministic reasons, otherwise if you rolled the same seed, you would get different outcomes.
|
|
local_dict: dict[str, int] = dict(zip(other_filler, weights_dict))
|
|
# local_dict["Present"] = 1
|
|
return self.random.choices(list(local_dict.keys()), list(local_dict.values()))[0]
|
|
|
|
# this handles ingame/client related things
|
|
def fill_slot_data(self):
|
|
return {
|
|
"unlimited_eggs": self.options.unlimited_eggs.value,
|
|
"ring_link": self.options.ring_link.value,
|
|
"starting_area": self.options.starting_area.value,
|
|
"exclude_environments": self.options.exclude_environments.value,
|
|
"giftsanity": self.options.giftsanity.value,
|
|
"progressive_vacuums": self.options.progressive_vacuums.value,
|
|
"missionsanity": self.options.missionsanity.value,
|
|
"supadow_minigames": self.options.supadow_minigames.value,
|
|
"move_rando": self.options.move_rando.value,
|
|
"moves_to_randomize": self.options.moves_to_randomize.value,
|
|
"gadget_rando": self.options.gadget_rando.value,
|
|
"gadgets_to_randomize": self.options.gadgets_to_randomize.value,
|
|
"exclude_gc": self.options.exclude_gc.value,
|
|
"progressive_gadgets": self.options.progressive_gadgets.value,
|
|
"killsanity": self.options.killsanity.value,
|
|
"misc_checks": self.options.misc_checks.value,
|
|
"death_link": self.options.death_link.value,
|
|
"damage_rate": self.options.damage_rate.value,
|
|
"music_rando": self.options.music_rando.value,
|
|
"chosen_music": self.songs_chosen,
|
|
}
|
|
|
|
def generate_output(self, output_directory: str) -> None:
|
|
# print("")
|
|
pass |