Files
Grinch-AP/worlds/osrs/__init__.py

666 lines
40 KiB
Python
Raw Normal View History

OSRS: Implement New Game (#1976) * MMBN3: Press program now has proper color index when received remotely * Initial commit of OSRS untangled from MMBN3 branch * Fixes some broken region connections * Removes some locations * Rearranges locations to fill in slots left by removed locations * Adds starting area rando * Moves Oak and Willow trees to resource regions * Fixes various PEP8 violations * Refactor of regions * Fixes variable capture issue with region rules * Partial completion of brutal grind logic * Finishes can_reach_skill function * Adds skill requirements to location rules, fixes regions rules * Adds documentation for OSRS * Removes match statement * Updates Data Version to test mode to prevent item name caching * Fixes starting spawn logic for east varrock * Fixes river lum crossing logic to not assume you can phase across water * Prevents equipping items when you haven't unlocked them * Changes canoe logic to not require huge levels * Skeletoning out some data I'll need for variable task system * Adds csvs and parser for logic * Adds Items parsing * Fixes the spawning logic to not default to Chunksanity when you didn't pick it * Begins adding generation rules for data-driven logic * Moves region handling and location creating to different methods * Adds logic limits to Options * Begun the location generation has * Randomly generates tasks for each skill until populated * Mopping up improper names, adding custom logic, and fixes location rolling * Drastically cleans up the location rolling loop * Modifies generation to properly use local variables and pass unit tests * Game is now generating, but rules don't seem to work * Lambda capture, my old nemesis. We meet again * Fixes issue with Corsair Cove item requirement causing logic loop * Okay one more fix, another variable capture * On second thought lets not have skull sceptre tasks. 'Tis a silly place * Removes QP from item pool (they're events not items) * Removes Stronghold floor tasks, no varbit to track them * Loads CSV with pkutil so it can be used in apworld * Fixes logic of skill tasks and adds QP requirements to long grinds * Fixes pathing in pkgutil call * Better handling for empty task categories, no longer throws errors * Fixes order for progressive tasks, removes un-checkable spider task * Fixes logic issues related to stew and the Blurite caves * Fixes issues generating causing tests to sporadically fail * Adds missing task that caused off-by-one error * Updates to new Options API * Updates generation to function properly with the Universal Tracker (Thanks Faris) * Replaces runtime CSV parsing with pre-made python files generated from CSVs * Switches to self.random and uses random.choice instead of doing it manually * Fixes to typing, variable names, iterators, and continue conditions * Replaces Name classes with Enums * Fixes parse error on region special rules * Skill requirements check now returns an accessrule instead of being one that checks options * Updates documentation and setup guide * Adjusts maximum numbers for combat and general tasks * Fixes region names so dictionary lookup works for chunksanity * Update worlds/osrs/docs/en_Old School Runescape.md Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> * Update worlds/osrs/docs/en_Old School Runescape.md Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> * Updates readme.md and codeowners doc * Removes erroneous East Varrock -> Al Kharid connection * Changes to canoe logic to account for woodcutting level options * Fixes embarassing typo on 'Edgeville' * Moves Logic CSVs to separate repository, addresses suggested changes on PR * Fixes logic error in east/west lumbridge regions. Fixes incorrect List typing in main * Removes task types with weight 0 from the list of rollable tasks * Missed another place that the task type had to be removed if 0 weight * Prevents adding an empty task weight if levels are too restrictive for tasks to be added * Removes giant blank space in error message * Adds player name to error for not having enough available tasks --------- Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>
2024-08-06 15:13:11 -06:00
import typing
from BaseClasses import Item, Tutorial, ItemClassification, Region, MultiWorld
from worlds.AutoWorld import WebWorld, World
from worlds.generic.Rules import add_rule, CollectionRule
from .Items import OSRSItem, starting_area_dict, chunksanity_starting_chunks, QP_Items, ItemRow, \
chunksanity_special_region_names
from .Locations import OSRSLocation, LocationRow
from .Options import OSRSOptions, StartingArea
from .Names import LocationNames, ItemNames, RegionNames
from .LogicCSV.LogicCSVToPython import data_csv_tag
from .LogicCSV.items_generated import item_rows
from .LogicCSV.locations_generated import location_rows
from .LogicCSV.regions_generated import region_rows
from .LogicCSV.resources_generated import resource_rows
from .Regions import RegionRow, ResourceRow
class OSRSWeb(WebWorld):
theme = "stone"
setup_en = Tutorial(
"Multiworld Setup Guide",
"A guide to setting up the Old School Runescape Randomizer connected to an Archipelago Multiworld",
"English",
"docs/setup_en.md",
"setup/en",
["digiholic"]
)
tutorials = [setup_en]
class OSRSWorld(World):
"""
The best retro fantasy MMORPG on the planet. Old School is RuneScape but older! This is the open world you know and love, but as it was in 2007.
The Randomizer takes the form of a Chunk-Restricted f2p Ironman that takes a brand new account up through defeating
the Green Dragon of Crandor and earning a spot in the fabled Champion's Guild!
"""
OSRS: Implement New Game (#1976) * MMBN3: Press program now has proper color index when received remotely * Initial commit of OSRS untangled from MMBN3 branch * Fixes some broken region connections * Removes some locations * Rearranges locations to fill in slots left by removed locations * Adds starting area rando * Moves Oak and Willow trees to resource regions * Fixes various PEP8 violations * Refactor of regions * Fixes variable capture issue with region rules * Partial completion of brutal grind logic * Finishes can_reach_skill function * Adds skill requirements to location rules, fixes regions rules * Adds documentation for OSRS * Removes match statement * Updates Data Version to test mode to prevent item name caching * Fixes starting spawn logic for east varrock * Fixes river lum crossing logic to not assume you can phase across water * Prevents equipping items when you haven't unlocked them * Changes canoe logic to not require huge levels * Skeletoning out some data I'll need for variable task system * Adds csvs and parser for logic * Adds Items parsing * Fixes the spawning logic to not default to Chunksanity when you didn't pick it * Begins adding generation rules for data-driven logic * Moves region handling and location creating to different methods * Adds logic limits to Options * Begun the location generation has * Randomly generates tasks for each skill until populated * Mopping up improper names, adding custom logic, and fixes location rolling * Drastically cleans up the location rolling loop * Modifies generation to properly use local variables and pass unit tests * Game is now generating, but rules don't seem to work * Lambda capture, my old nemesis. We meet again * Fixes issue with Corsair Cove item requirement causing logic loop * Okay one more fix, another variable capture * On second thought lets not have skull sceptre tasks. 'Tis a silly place * Removes QP from item pool (they're events not items) * Removes Stronghold floor tasks, no varbit to track them * Loads CSV with pkutil so it can be used in apworld * Fixes logic of skill tasks and adds QP requirements to long grinds * Fixes pathing in pkgutil call * Better handling for empty task categories, no longer throws errors * Fixes order for progressive tasks, removes un-checkable spider task * Fixes logic issues related to stew and the Blurite caves * Fixes issues generating causing tests to sporadically fail * Adds missing task that caused off-by-one error * Updates to new Options API * Updates generation to function properly with the Universal Tracker (Thanks Faris) * Replaces runtime CSV parsing with pre-made python files generated from CSVs * Switches to self.random and uses random.choice instead of doing it manually * Fixes to typing, variable names, iterators, and continue conditions * Replaces Name classes with Enums * Fixes parse error on region special rules * Skill requirements check now returns an accessrule instead of being one that checks options * Updates documentation and setup guide * Adjusts maximum numbers for combat and general tasks * Fixes region names so dictionary lookup works for chunksanity * Update worlds/osrs/docs/en_Old School Runescape.md Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> * Update worlds/osrs/docs/en_Old School Runescape.md Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> * Updates readme.md and codeowners doc * Removes erroneous East Varrock -> Al Kharid connection * Changes to canoe logic to account for woodcutting level options * Fixes embarassing typo on 'Edgeville' * Moves Logic CSVs to separate repository, addresses suggested changes on PR * Fixes logic error in east/west lumbridge regions. Fixes incorrect List typing in main * Removes task types with weight 0 from the list of rollable tasks * Missed another place that the task type had to be removed if 0 weight * Prevents adding an empty task weight if levels are too restrictive for tasks to be added * Removes giant blank space in error message * Adds player name to error for not having enough available tasks --------- Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>
2024-08-06 15:13:11 -06:00
game = "Old School Runescape"
options_dataclass = OSRSOptions
options: OSRSOptions
topology_present = True
web = OSRSWeb()
base_id = 0x070000
data_version = 1
item_name_to_id = {item_rows[i].name: 0x070000 + i for i in range(len(item_rows))}
location_name_to_id = {location_rows[i].name: 0x070000 + i for i in range(len(location_rows))}
region_name_to_data: typing.Dict[str, Region]
location_name_to_data: typing.Dict[str, OSRSLocation]
location_rows_by_name: typing.Dict[str, LocationRow]
region_rows_by_name: typing.Dict[str, RegionRow]
resource_rows_by_name: typing.Dict[str, ResourceRow]
item_rows_by_name: typing.Dict[str, ItemRow]
starting_area_item: str
locations_by_category: typing.Dict[str, typing.List[LocationRow]]
def __init__(self, multiworld: MultiWorld, player: int):
super().__init__(multiworld, player)
OSRS: Implement New Game (#1976) * MMBN3: Press program now has proper color index when received remotely * Initial commit of OSRS untangled from MMBN3 branch * Fixes some broken region connections * Removes some locations * Rearranges locations to fill in slots left by removed locations * Adds starting area rando * Moves Oak and Willow trees to resource regions * Fixes various PEP8 violations * Refactor of regions * Fixes variable capture issue with region rules * Partial completion of brutal grind logic * Finishes can_reach_skill function * Adds skill requirements to location rules, fixes regions rules * Adds documentation for OSRS * Removes match statement * Updates Data Version to test mode to prevent item name caching * Fixes starting spawn logic for east varrock * Fixes river lum crossing logic to not assume you can phase across water * Prevents equipping items when you haven't unlocked them * Changes canoe logic to not require huge levels * Skeletoning out some data I'll need for variable task system * Adds csvs and parser for logic * Adds Items parsing * Fixes the spawning logic to not default to Chunksanity when you didn't pick it * Begins adding generation rules for data-driven logic * Moves region handling and location creating to different methods * Adds logic limits to Options * Begun the location generation has * Randomly generates tasks for each skill until populated * Mopping up improper names, adding custom logic, and fixes location rolling * Drastically cleans up the location rolling loop * Modifies generation to properly use local variables and pass unit tests * Game is now generating, but rules don't seem to work * Lambda capture, my old nemesis. We meet again * Fixes issue with Corsair Cove item requirement causing logic loop * Okay one more fix, another variable capture * On second thought lets not have skull sceptre tasks. 'Tis a silly place * Removes QP from item pool (they're events not items) * Removes Stronghold floor tasks, no varbit to track them * Loads CSV with pkutil so it can be used in apworld * Fixes logic of skill tasks and adds QP requirements to long grinds * Fixes pathing in pkgutil call * Better handling for empty task categories, no longer throws errors * Fixes order for progressive tasks, removes un-checkable spider task * Fixes logic issues related to stew and the Blurite caves * Fixes issues generating causing tests to sporadically fail * Adds missing task that caused off-by-one error * Updates to new Options API * Updates generation to function properly with the Universal Tracker (Thanks Faris) * Replaces runtime CSV parsing with pre-made python files generated from CSVs * Switches to self.random and uses random.choice instead of doing it manually * Fixes to typing, variable names, iterators, and continue conditions * Replaces Name classes with Enums * Fixes parse error on region special rules * Skill requirements check now returns an accessrule instead of being one that checks options * Updates documentation and setup guide * Adjusts maximum numbers for combat and general tasks * Fixes region names so dictionary lookup works for chunksanity * Update worlds/osrs/docs/en_Old School Runescape.md Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> * Update worlds/osrs/docs/en_Old School Runescape.md Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> * Updates readme.md and codeowners doc * Removes erroneous East Varrock -> Al Kharid connection * Changes to canoe logic to account for woodcutting level options * Fixes embarassing typo on 'Edgeville' * Moves Logic CSVs to separate repository, addresses suggested changes on PR * Fixes logic error in east/west lumbridge regions. Fixes incorrect List typing in main * Removes task types with weight 0 from the list of rollable tasks * Missed another place that the task type had to be removed if 0 weight * Prevents adding an empty task weight if levels are too restrictive for tasks to be added * Removes giant blank space in error message * Adds player name to error for not having enough available tasks --------- Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>
2024-08-06 15:13:11 -06:00
self.region_name_to_data = {}
self.location_name_to_data = {}
self.location_rows_by_name = {}
self.region_rows_by_name = {}
self.resource_rows_by_name = {}
self.item_rows_by_name = {}
self.starting_area_item = ""
self.locations_by_category = {}
def generate_early(self) -> None:
location_categories = [location_row.category for location_row in location_rows]
self.locations_by_category = {category:
[location_row for location_row in location_rows if
location_row.category == category]
for category in location_categories}
self.location_rows_by_name = {loc_row.name: loc_row for loc_row in location_rows}
self.region_rows_by_name = {reg_row.name: reg_row for reg_row in region_rows}
self.resource_rows_by_name = {rec_row.name: rec_row for rec_row in resource_rows}
self.item_rows_by_name = {it_row.name: it_row for it_row in item_rows}
rnd = self.random
starting_area = self.options.starting_area
if starting_area.value == StartingArea.option_any_bank:
self.starting_area_item = rnd.choice(starting_area_dict)
elif starting_area.value < StartingArea.option_chunksanity:
self.starting_area_item = starting_area_dict[starting_area.value]
else:
self.starting_area_item = rnd.choice(chunksanity_starting_chunks)
# Set Starting Chunk
self.multiworld.push_precollected(self.create_item(self.starting_area_item))
"""
This function pulls from LogicCSVToPython so that it sends the correct tag of the repository to the client.
_Make sure to update that value whenever the CSVs change!_
"""
def fill_slot_data(self):
data = self.options.as_dict("brutal_grinds")
data["data_csv_tag"] = data_csv_tag
return data
def create_regions(self) -> None:
"""
called to place player's regions into the MultiWorld's regions list. If it's hard to separate, this can be done
during generate_early or basic as well.
"""
# First, create the "Menu" region to start
menu_region = self.create_region("Menu")
for region_row in region_rows:
self.create_region(region_row.name)
for resource_row in resource_rows:
self.create_region(resource_row.name)
# Removes the word "Area: " from the item name to get the region it applies to.
# I figured tacking "Area: " at the beginning would make it _easier_ to tell apart. Turns out it made it worse
if self.starting_area_item in chunksanity_special_region_names:
starting_area_region = chunksanity_special_region_names[self.starting_area_item]
else:
starting_area_region = self.starting_area_item[6:] # len("Area: ")
starting_entrance = menu_region.create_exit(f"Start->{starting_area_region}")
starting_entrance.access_rule = lambda state: state.has(self.starting_area_item, self.player)
starting_entrance.connect(self.region_name_to_data[starting_area_region])
# Create entrances between regions
for region_row in region_rows:
region = self.region_name_to_data[region_row.name]
for outbound_region_name in region_row.connections:
parsed_outbound = outbound_region_name.replace('*', '')
entrance = region.create_exit(f"{region_row.name}->{parsed_outbound}")
entrance.connect(self.region_name_to_data[parsed_outbound])
item_name = self.region_rows_by_name[parsed_outbound].itemReq
if "*" not in outbound_region_name and "*" not in item_name:
entrance.access_rule = lambda state, item_name=item_name: state.has(item_name, self.player)
continue
self.generate_special_rules_for(entrance, region_row, outbound_region_name)
for resource_region in region_row.resources:
if not resource_region:
continue
entrance = region.create_exit(f"{region_row.name}->{resource_region.replace('*', '')}")
if "*" not in resource_region:
entrance.connect(self.region_name_to_data[resource_region])
else:
self.generate_special_rules_for(entrance, region_row, resource_region)
entrance.connect(self.region_name_to_data[resource_region.replace('*', '')])
self.roll_locations()
def generate_special_rules_for(self, entrance, region_row, outbound_region_name):
# print(f"Special rules required to access region {outbound_region_name} from {region_row.name}")
if outbound_region_name == RegionNames.Cooks_Guild:
item_name = self.region_rows_by_name[outbound_region_name].itemReq.replace('*', '')
cooking_level_rule = self.get_skill_rule("cooking", 32)
entrance.access_rule = lambda state: state.has(item_name, self.player) and \
cooking_level_rule(state)
return
if outbound_region_name == RegionNames.Crafting_Guild:
item_name = self.region_rows_by_name[outbound_region_name].itemReq.replace('*', '')
crafting_level_rule = self.get_skill_rule("crafting", 40)
entrance.access_rule = lambda state: state.has(item_name, self.player) and \
crafting_level_rule(state)
return
if outbound_region_name == RegionNames.Corsair_Cove:
item_name = self.region_rows_by_name[outbound_region_name].itemReq.replace('*', '')
# Need to be able to start Corsair Curse in addition to having the item
entrance.access_rule = lambda state: state.has(item_name, self.player) and \
state.can_reach(RegionNames.Falador_Farm, "Region", self.player)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.Falador_Farm, self.player), entrance)
return
if outbound_region_name == "Camdozaal*":
item_name = self.region_rows_by_name[outbound_region_name.replace('*', '')].itemReq
entrance.access_rule = lambda state: state.has(item_name, self.player) and \
state.has(ItemNames.QP_Below_Ice_Mountain, self.player)
return
if region_row.name == "Dwarven Mountain Pass" and outbound_region_name == "Anvil*":
entrance.access_rule = lambda state: state.has(ItemNames.QP_Dorics_Quest, self.player)
return
# Special logic for canoes
canoe_regions = [RegionNames.Lumbridge, RegionNames.South_Of_Varrock, RegionNames.Barbarian_Village,
RegionNames.Edgeville, RegionNames.Wilderness]
if region_row.name in canoe_regions:
# Skill rules for greater distances
woodcutting_rule_d1 = self.get_skill_rule("woodcutting", 12)
woodcutting_rule_d2 = self.get_skill_rule("woodcutting", 27)
woodcutting_rule_d3 = self.get_skill_rule("woodcutting", 42)
woodcutting_rule_all = self.get_skill_rule("woodcutting", 57)
if region_row.name == RegionNames.Lumbridge:
# Canoe Tree access for the Location
if outbound_region_name == RegionNames.Canoe_Tree:
entrance.access_rule = \
lambda state: (state.can_reach_region(RegionNames.South_Of_Varrock, self.player)
and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \
(state.can_reach_region(RegionNames.Barbarian_Village)
and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) or \
(state.can_reach_region(RegionNames.Edgeville)
and woodcutting_rule_d3(state) and self.options.max_woodcutting_level >= 42) or \
(state.can_reach_region(RegionNames.Wilderness)
and woodcutting_rule_all(state) and self.options.max_woodcutting_level >= 57)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.South_Of_Varrock, self.player), entrance)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.Barbarian_Village, self.player), entrance)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.Edgeville, self.player), entrance)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.Wilderness, self.player), entrance)
# Access to other chunks based on woodcutting settings
# South of Varrock does not need to be checked, because it's already adjacent
if outbound_region_name == RegionNames.Barbarian_Village:
entrance.access_rule = lambda state: woodcutting_rule_d2(state) \
and self.options.max_woodcutting_level >= 27
if outbound_region_name == RegionNames.Edgeville:
entrance.access_rule = lambda state: woodcutting_rule_d3(state) \
and self.options.max_woodcutting_level >= 42
if outbound_region_name == RegionNames.Wilderness:
entrance.access_rule = lambda state: woodcutting_rule_all(state) \
and self.options.max_woodcutting_level >= 57
if region_row.name == RegionNames.South_Of_Varrock:
if outbound_region_name == RegionNames.Canoe_Tree:
entrance.access_rule = \
lambda state: (state.can_reach_region(RegionNames.Lumbridge, self.player)
and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \
(state.can_reach_region(RegionNames.Barbarian_Village)
and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \
(state.can_reach_region(RegionNames.Edgeville)
and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) or \
(state.can_reach_region(RegionNames.Wilderness)
and woodcutting_rule_d3(state) and self.options.max_woodcutting_level >= 42)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.Lumbridge, self.player), entrance)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.Barbarian_Village, self.player), entrance)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.Edgeville, self.player), entrance)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.Wilderness, self.player), entrance)
# Access to other chunks based on woodcutting settings
# Lumbridge does not need to be checked, because it's already adjacent
if outbound_region_name == RegionNames.Barbarian_Village:
entrance.access_rule = lambda state: woodcutting_rule_d1(state) \
and self.options.max_woodcutting_level >= 12
if outbound_region_name == RegionNames.Edgeville:
entrance.access_rule = lambda state: woodcutting_rule_d3(state) \
and self.options.max_woodcutting_level >= 27
if outbound_region_name == RegionNames.Wilderness:
entrance.access_rule = lambda state: woodcutting_rule_all(state) \
and self.options.max_woodcutting_level >= 42
if region_row.name == RegionNames.Barbarian_Village:
if outbound_region_name == RegionNames.Canoe_Tree:
entrance.access_rule = \
lambda state: (state.can_reach_region(RegionNames.Lumbridge, self.player)
and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) or \
(state.can_reach_region(RegionNames.South_Of_Varrock)
and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \
(state.can_reach_region(RegionNames.Edgeville)
and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \
(state.can_reach_region(RegionNames.Wilderness)
and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.Lumbridge, self.player), entrance)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.South_Of_Varrock, self.player), entrance)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.Edgeville, self.player), entrance)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.Wilderness, self.player), entrance)
# Access to other chunks based on woodcutting settings
if outbound_region_name == RegionNames.Lumbridge:
entrance.access_rule = lambda state: woodcutting_rule_d2(state) \
and self.options.max_woodcutting_level >= 27
if outbound_region_name == RegionNames.South_Of_Varrock:
entrance.access_rule = lambda state: woodcutting_rule_d1(state) \
and self.options.max_woodcutting_level >= 12
# Edgeville does not need to be checked, because it's already adjacent
if outbound_region_name == RegionNames.Wilderness:
entrance.access_rule = lambda state: woodcutting_rule_d3(state) \
and self.options.max_woodcutting_level >= 42
if region_row.name == RegionNames.Edgeville:
if outbound_region_name == RegionNames.Canoe_Tree:
entrance.access_rule = \
lambda state: (state.can_reach_region(RegionNames.Lumbridge, self.player)
and woodcutting_rule_d3(state) and self.options.max_woodcutting_level >= 42) or \
(state.can_reach_region(RegionNames.South_Of_Varrock)
and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) or \
(state.can_reach_region(RegionNames.Barbarian_Village)
and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \
(state.can_reach_region(RegionNames.Wilderness)
and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.Lumbridge, self.player), entrance)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.South_Of_Varrock, self.player), entrance)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.Barbarian_Village, self.player), entrance)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.Wilderness, self.player), entrance)
# Access to other chunks based on woodcutting settings
if outbound_region_name == RegionNames.Lumbridge:
entrance.access_rule = lambda state: woodcutting_rule_d3(state) \
and self.options.max_woodcutting_level >= 42
if outbound_region_name == RegionNames.South_Of_Varrock:
entrance.access_rule = lambda state: woodcutting_rule_d2(state) \
and self.options.max_woodcutting_level >= 27
# Barbarian Village does not need to be checked, because it's already adjacent
# Wilderness does not need to be checked, because it's already adjacent
if region_row.name == RegionNames.Wilderness:
if outbound_region_name == RegionNames.Canoe_Tree:
entrance.access_rule = \
lambda state: (state.can_reach_region(RegionNames.Lumbridge, self.player)
and woodcutting_rule_all(state) and self.options.max_woodcutting_level >= 57) or \
(state.can_reach_region(RegionNames.South_Of_Varrock)
and woodcutting_rule_d3(state) and self.options.max_woodcutting_level >= 42) or \
(state.can_reach_region(RegionNames.Barbarian_Village)
and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) or \
(state.can_reach_region(RegionNames.Edgeville)
and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.Lumbridge, self.player), entrance)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.South_Of_Varrock, self.player), entrance)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.Barbarian_Village, self.player), entrance)
self.multiworld.register_indirect_condition(
self.multiworld.get_region(RegionNames.Edgeville, self.player), entrance)
# Access to other chunks based on woodcutting settings
if outbound_region_name == RegionNames.Lumbridge:
entrance.access_rule = lambda state: woodcutting_rule_all(state) \
and self.options.max_woodcutting_level >= 57
if outbound_region_name == RegionNames.South_Of_Varrock:
entrance.access_rule = lambda state: woodcutting_rule_d3(state) \
and self.options.max_woodcutting_level >= 42
if outbound_region_name == RegionNames.Barbarian_Village:
entrance.access_rule = lambda state: woodcutting_rule_d2(state) \
and self.options.max_woodcutting_level >= 27
# Edgeville does not need to be checked, because it's already adjacent
def roll_locations(self):
locations_required = 0
generation_is_fake = hasattr(self.multiworld, "generation_is_fake") # UT specific override
for item_row in item_rows:
locations_required += item_row.amount
locations_added = 1 # At this point we've already added the starting area, so we start at 1 instead of 0
# Quests are always added
for i, location_row in enumerate(location_rows):
if location_row.category in {"quest", "points", "goal"}:
self.create_and_add_location(i)
if location_row.category == "quest":
locations_added += 1
# Build up the weighted Task Pool
rnd = self.random
# Start with the minimum general tasks
general_tasks = [task for task in self.locations_by_category["general"]]
if not self.options.progressive_tasks:
rnd.shuffle(general_tasks)
else:
general_tasks.reverse()
for i in range(self.options.minimum_general_tasks):
task = general_tasks.pop()
self.add_location(task)
locations_added += 1
general_weight = self.options.general_task_weight if len(general_tasks) > 0 else 0
tasks_per_task_type: typing.Dict[str, typing.List[LocationRow]] = {}
weights_per_task_type: typing.Dict[str, int] = {}
task_types = ["prayer", "magic", "runecraft", "mining", "crafting",
"smithing", "fishing", "cooking", "firemaking", "woodcutting", "combat"]
for task_type in task_types:
max_level_for_task_type = getattr(self.options, f"max_{task_type}_level")
max_amount_for_task_type = getattr(self.options, f"max_{task_type}_tasks")
tasks_for_this_type = [task for task in self.locations_by_category[task_type]
if task.skills[0].level <= max_level_for_task_type]
if not self.options.progressive_tasks:
rnd.shuffle(tasks_for_this_type)
else:
tasks_for_this_type.reverse()
tasks_for_this_type = tasks_for_this_type[:max_amount_for_task_type]
weight_for_this_type = getattr(self.options,
f"{task_type}_task_weight")
if weight_for_this_type > 0 and tasks_for_this_type:
tasks_per_task_type[task_type] = tasks_for_this_type
weights_per_task_type[task_type] = weight_for_this_type
# Build a list of collections and weights in a matching order for rnd.choices later
all_tasks = []
all_weights = []
for task_type in task_types:
if task_type in tasks_per_task_type:
all_tasks.append(tasks_per_task_type[task_type])
all_weights.append(weights_per_task_type[task_type])
# Even after the initial forced generals, they can still be rolled randomly
if general_weight > 0:
all_tasks.append(general_tasks)
all_weights.append(general_weight)
while locations_added < locations_required or (generation_is_fake and len(all_tasks) > 0):
if all_tasks:
chosen_task = rnd.choices(all_tasks, all_weights)[0]
if chosen_task:
task = chosen_task.pop()
self.add_location(task)
locations_added += 1
# This isn't an else because chosen_task can become empty in the process of resolving the above block
# We still want to clear this list out while we're doing that
if not chosen_task:
index = all_tasks.index(chosen_task)
del all_tasks[index]
del all_weights[index]
else:
if len(general_tasks) == 0:
raise Exception(f"There are not enough available tasks to fill the remaining pool for OSRS " +
f"Please adjust {self.player_name}'s settings to be less restrictive of tasks.")
task = general_tasks.pop()
self.add_location(task)
locations_added += 1
def add_location(self, location):
index = [i for i in range(len(location_rows)) if location_rows[i].name == location.name][0]
self.create_and_add_location(index)
def create_items(self) -> None:
for item_row in item_rows:
if item_row.name != self.starting_area_item:
for c in range(item_row.amount):
item = self.create_item(item_row.name)
self.multiworld.itempool.append(item)
def get_filler_item_name(self) -> str:
return self.random.choice(
[ItemNames.Progressive_Armor, ItemNames.Progressive_Weapons, ItemNames.Progressive_Magic,
ItemNames.Progressive_Tools, ItemNames.Progressive_Range_Armor, ItemNames.Progressive_Range_Weapon])
def create_and_add_location(self, row_index) -> None:
location_row = location_rows[row_index]
# print(f"Adding task {location_row.name}")
# Create Location
location_id = self.base_id + row_index
if location_row.category == "points" or location_row.category == "goal":
location_id = None
location = OSRSLocation(self.player, location_row.name, location_id)
self.location_name_to_data[location_row.name] = location
# Add the location to its first region, or if it doesn't belong to one, to Menu
region = self.region_name_to_data["Menu"]
if location_row.regions:
region = self.region_name_to_data[location_row.regions[0]]
location.parent_region = region
region.locations.append(location)
def set_rules(self) -> None:
"""
called to set access and item rules on locations and entrances.
"""
quest_attr_names = ["Cooks_Assistant", "Demon_Slayer", "Restless_Ghost", "Romeo_Juliet",
"Sheep_Shearer", "Shield_of_Arrav", "Ernest_the_Chicken", "Vampyre_Slayer",
"Imp_Catcher", "Prince_Ali_Rescue", "Dorics_Quest", "Black_Knights_Fortress",
"Witchs_Potion", "Knights_Sword", "Goblin_Diplomacy", "Pirates_Treasure",
"Rune_Mysteries", "Misthalin_Mystery", "Corsair_Curse", "X_Marks_the_Spot",
"Below_Ice_Mountain"]
for qp_attr_name in quest_attr_names:
loc_name = getattr(LocationNames, f"QP_{qp_attr_name}")
item_name = getattr(ItemNames, f"QP_{qp_attr_name}")
self.multiworld.get_location(loc_name, self.player) \
.place_locked_item(self.create_event(item_name))
for quest_attr_name in quest_attr_names:
qp_loc_name = getattr(LocationNames, f"QP_{quest_attr_name}")
q_loc_name = getattr(LocationNames, f"Q_{quest_attr_name}")
add_rule(self.multiworld.get_location(qp_loc_name, self.player), lambda state, q_loc_name=q_loc_name: (
self.multiworld.get_location(q_loc_name, self.player).can_reach(state)
))
# place "Victory" at "Dragon Slayer" and set collection as win condition
self.multiworld.get_location(LocationNames.Q_Dragon_Slayer, self.player) \
.place_locked_item(self.create_event("Victory"))
self.multiworld.completion_condition[self.player] = lambda state: (state.has("Victory", self.player))
for location_name, location in self.location_name_to_data.items():
location_row = self.location_rows_by_name[location_name]
# Set up requirements for region
for region_required_name in location_row.regions:
region_required = self.region_name_to_data[region_required_name]
add_rule(location,
lambda state, region_required=region_required: state.can_reach(region_required, "Region",
self.player))
for skill_req in location_row.skills:
add_rule(location, self.get_skill_rule(skill_req.skill, skill_req.level))
for item_req in location_row.items:
add_rule(location, lambda state, item_req=item_req: state.has(item_req, self.player))
if location_row.qp:
add_rule(location, lambda state, location_row=location_row: self.quest_points(state) > location_row.qp)
def create_region(self, name: str) -> "Region":
region = Region(name, self.player, self.multiworld)
self.region_name_to_data[name] = region
self.multiworld.regions.append(region)
return region
def create_item(self, item_name: str) -> "Item":
items = [item for item in item_rows if item.name == item_name]
assert len(items) > 0, f"No matching item found for name {item_name} for player {self.player_name}"
item = items[0]
OSRS: Implement New Game (#1976) * MMBN3: Press program now has proper color index when received remotely * Initial commit of OSRS untangled from MMBN3 branch * Fixes some broken region connections * Removes some locations * Rearranges locations to fill in slots left by removed locations * Adds starting area rando * Moves Oak and Willow trees to resource regions * Fixes various PEP8 violations * Refactor of regions * Fixes variable capture issue with region rules * Partial completion of brutal grind logic * Finishes can_reach_skill function * Adds skill requirements to location rules, fixes regions rules * Adds documentation for OSRS * Removes match statement * Updates Data Version to test mode to prevent item name caching * Fixes starting spawn logic for east varrock * Fixes river lum crossing logic to not assume you can phase across water * Prevents equipping items when you haven't unlocked them * Changes canoe logic to not require huge levels * Skeletoning out some data I'll need for variable task system * Adds csvs and parser for logic * Adds Items parsing * Fixes the spawning logic to not default to Chunksanity when you didn't pick it * Begins adding generation rules for data-driven logic * Moves region handling and location creating to different methods * Adds logic limits to Options * Begun the location generation has * Randomly generates tasks for each skill until populated * Mopping up improper names, adding custom logic, and fixes location rolling * Drastically cleans up the location rolling loop * Modifies generation to properly use local variables and pass unit tests * Game is now generating, but rules don't seem to work * Lambda capture, my old nemesis. We meet again * Fixes issue with Corsair Cove item requirement causing logic loop * Okay one more fix, another variable capture * On second thought lets not have skull sceptre tasks. 'Tis a silly place * Removes QP from item pool (they're events not items) * Removes Stronghold floor tasks, no varbit to track them * Loads CSV with pkutil so it can be used in apworld * Fixes logic of skill tasks and adds QP requirements to long grinds * Fixes pathing in pkgutil call * Better handling for empty task categories, no longer throws errors * Fixes order for progressive tasks, removes un-checkable spider task * Fixes logic issues related to stew and the Blurite caves * Fixes issues generating causing tests to sporadically fail * Adds missing task that caused off-by-one error * Updates to new Options API * Updates generation to function properly with the Universal Tracker (Thanks Faris) * Replaces runtime CSV parsing with pre-made python files generated from CSVs * Switches to self.random and uses random.choice instead of doing it manually * Fixes to typing, variable names, iterators, and continue conditions * Replaces Name classes with Enums * Fixes parse error on region special rules * Skill requirements check now returns an accessrule instead of being one that checks options * Updates documentation and setup guide * Adjusts maximum numbers for combat and general tasks * Fixes region names so dictionary lookup works for chunksanity * Update worlds/osrs/docs/en_Old School Runescape.md Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> * Update worlds/osrs/docs/en_Old School Runescape.md Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> * Updates readme.md and codeowners doc * Removes erroneous East Varrock -> Al Kharid connection * Changes to canoe logic to account for woodcutting level options * Fixes embarassing typo on 'Edgeville' * Moves Logic CSVs to separate repository, addresses suggested changes on PR * Fixes logic error in east/west lumbridge regions. Fixes incorrect List typing in main * Removes task types with weight 0 from the list of rollable tasks * Missed another place that the task type had to be removed if 0 weight * Prevents adding an empty task weight if levels are too restrictive for tasks to be added * Removes giant blank space in error message * Adds player name to error for not having enough available tasks --------- Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>
2024-08-06 15:13:11 -06:00
index = item_rows.index(item)
return OSRSItem(item.name, item.progression, self.base_id + index, self.player)
def create_event(self, event: str):
# while we are at it, we can also add a helper to create events
return OSRSItem(event, ItemClassification.progression, None, self.player)
def quest_points(self, state):
qp = 0
for qp_event in QP_Items:
if state.has(qp_event, self.player):
qp += int(qp_event[0])
return qp
"""
Ensures a target level can be reached with available resources
"""
def get_skill_rule(self, skill, level) -> CollectionRule:
if skill.lower() == "fishing":
if self.options.brutal_grinds or level < 5:
return lambda state: state.can_reach(RegionNames.Shrimp, "Region", self.player)
if level < 20:
return lambda state: state.can_reach(RegionNames.Shrimp, "Region", self.player) and \
state.can_reach(RegionNames.Port_Sarim, "Region", self.player)
else:
return lambda state: state.can_reach(RegionNames.Shrimp, "Region", self.player) and \
state.can_reach(RegionNames.Port_Sarim, "Region", self.player) and \
state.can_reach(RegionNames.Fly_Fish, "Region", self.player)
if skill.lower() == "mining":
if self.options.brutal_grinds or level < 15:
return lambda state: state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) or \
state.can_reach(RegionNames.Clay_Rock, "Region", self.player)
else:
# Iron is the best way to train all the way to 99, so having access to iron is all you need to check for
return lambda state: (state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) or
state.can_reach(RegionNames.Clay_Rock, "Region", self.player)) and \
state.can_reach(RegionNames.Iron_Rock, "Region", self.player)
if skill.lower() == "woodcutting":
if self.options.brutal_grinds or level < 15:
# I've checked. There is not a single chunk in the f2p that does not have at least one normal tree.
# Even the desert.
return lambda state: True
if level < 30:
return lambda state: state.can_reach(RegionNames.Oak_Tree, "Region", self.player)
else:
return lambda state: state.can_reach(RegionNames.Oak_Tree, "Region", self.player) and \
state.can_reach(RegionNames.Willow_Tree, "Region", self.player)
if skill.lower() == "smithing":
if self.options.brutal_grinds:
return lambda state: state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) and \
state.can_reach(RegionNames.Furnace, "Region", self.player)
if level < 15:
# Lumbridge has a special bronze-only anvil. This is the only anvil of its type so it's not included
# in the "Anvil" resource region. We still need to check for it though.
return lambda state: state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) and \
state.can_reach(RegionNames.Furnace, "Region", self.player) and \
(state.can_reach(RegionNames.Anvil, "Region", self.player) or
state.can_reach(RegionNames.Lumbridge, "Region", self.player))
if level < 30:
# For levels between 15 and 30, the lumbridge anvil won't cut it. Only a real one will do
return lambda state: state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) and \
state.can_reach(RegionNames.Iron_Rock, "Region", self.player) and \
state.can_reach(RegionNames.Furnace, "Region", self.player) and \
state.can_reach(RegionNames.Anvil, "Region", self.player)
else:
return lambda state: state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) and \
state.can_reach(RegionNames.Iron_Rock, "Region", self.player) and \
state.can_reach(RegionNames.Coal_Rock, "Region", self.player) and \
state.can_reach(RegionNames.Furnace, "Region", self.player) and \
state.can_reach(RegionNames.Anvil, "Region", self.player)
if skill.lower() == "crafting":
# Crafting is really complex. Need a lot of sub-rules to make this even remotely readable
def can_spin(state):
return state.can_reach(RegionNames.Sheep, "Region", self.player) and \
state.can_reach(RegionNames.Spinning_Wheel, "Region", self.player)
def can_pot(state):
return state.can_reach(RegionNames.Clay_Rock, "Region", self.player) and \
state.can_reach(RegionNames.Barbarian_Village, "Region", self.player)
def can_tan(state):
return state.can_reach(RegionNames.Milk, "Region", self.player) and \
state.can_reach(RegionNames.Al_Kharid, "Region", self.player)
def mould_access(state):
return state.can_reach(RegionNames.Al_Kharid, "Region", self.player) or \
state.can_reach(RegionNames.Rimmington, "Region", self.player)
def can_silver(state):
return state.can_reach(RegionNames.Silver_Rock, "Region", self.player) and \
state.can_reach(RegionNames.Furnace, "Region", self.player) and mould_access(state)
def can_gold(state):
return state.can_reach(RegionNames.Gold_Rock, "Region", self.player) and \
state.can_reach(RegionNames.Furnace, "Region", self.player) and mould_access(state)
if self.options.brutal_grinds or level < 5:
return lambda state: can_spin(state) or can_pot(state) or can_tan(state)
can_smelt_gold = self.get_skill_rule("smithing", 40)
can_smelt_silver = self.get_skill_rule("smithing", 20)
if level < 16:
return lambda state: can_pot(state) or can_tan(state) or (can_gold(state) and can_smelt_gold(state))
else:
return lambda state: can_tan(state) or (can_silver(state) and can_smelt_silver(state)) or \
(can_gold(state) and can_smelt_gold(state))
if skill.lower() == "cooking":
OSRS: Implement New Game (#1976) * MMBN3: Press program now has proper color index when received remotely * Initial commit of OSRS untangled from MMBN3 branch * Fixes some broken region connections * Removes some locations * Rearranges locations to fill in slots left by removed locations * Adds starting area rando * Moves Oak and Willow trees to resource regions * Fixes various PEP8 violations * Refactor of regions * Fixes variable capture issue with region rules * Partial completion of brutal grind logic * Finishes can_reach_skill function * Adds skill requirements to location rules, fixes regions rules * Adds documentation for OSRS * Removes match statement * Updates Data Version to test mode to prevent item name caching * Fixes starting spawn logic for east varrock * Fixes river lum crossing logic to not assume you can phase across water * Prevents equipping items when you haven't unlocked them * Changes canoe logic to not require huge levels * Skeletoning out some data I'll need for variable task system * Adds csvs and parser for logic * Adds Items parsing * Fixes the spawning logic to not default to Chunksanity when you didn't pick it * Begins adding generation rules for data-driven logic * Moves region handling and location creating to different methods * Adds logic limits to Options * Begun the location generation has * Randomly generates tasks for each skill until populated * Mopping up improper names, adding custom logic, and fixes location rolling * Drastically cleans up the location rolling loop * Modifies generation to properly use local variables and pass unit tests * Game is now generating, but rules don't seem to work * Lambda capture, my old nemesis. We meet again * Fixes issue with Corsair Cove item requirement causing logic loop * Okay one more fix, another variable capture * On second thought lets not have skull sceptre tasks. 'Tis a silly place * Removes QP from item pool (they're events not items) * Removes Stronghold floor tasks, no varbit to track them * Loads CSV with pkutil so it can be used in apworld * Fixes logic of skill tasks and adds QP requirements to long grinds * Fixes pathing in pkgutil call * Better handling for empty task categories, no longer throws errors * Fixes order for progressive tasks, removes un-checkable spider task * Fixes logic issues related to stew and the Blurite caves * Fixes issues generating causing tests to sporadically fail * Adds missing task that caused off-by-one error * Updates to new Options API * Updates generation to function properly with the Universal Tracker (Thanks Faris) * Replaces runtime CSV parsing with pre-made python files generated from CSVs * Switches to self.random and uses random.choice instead of doing it manually * Fixes to typing, variable names, iterators, and continue conditions * Replaces Name classes with Enums * Fixes parse error on region special rules * Skill requirements check now returns an accessrule instead of being one that checks options * Updates documentation and setup guide * Adjusts maximum numbers for combat and general tasks * Fixes region names so dictionary lookup works for chunksanity * Update worlds/osrs/docs/en_Old School Runescape.md Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> * Update worlds/osrs/docs/en_Old School Runescape.md Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> * Updates readme.md and codeowners doc * Removes erroneous East Varrock -> Al Kharid connection * Changes to canoe logic to account for woodcutting level options * Fixes embarassing typo on 'Edgeville' * Moves Logic CSVs to separate repository, addresses suggested changes on PR * Fixes logic error in east/west lumbridge regions. Fixes incorrect List typing in main * Removes task types with weight 0 from the list of rollable tasks * Missed another place that the task type had to be removed if 0 weight * Prevents adding an empty task weight if levels are too restrictive for tasks to be added * Removes giant blank space in error message * Adds player name to error for not having enough available tasks --------- Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>
2024-08-06 15:13:11 -06:00
if self.options.brutal_grinds or level < 15:
return lambda state: state.can_reach(RegionNames.Milk, "Region", self.player) or \
state.can_reach(RegionNames.Egg, "Region", self.player) or \
state.can_reach(RegionNames.Shrimp, "Region", self.player) or \
(state.can_reach(RegionNames.Wheat, "Region", self.player) and
state.can_reach(RegionNames.Windmill, "Region", self.player))
else:
can_catch_fly_fish = self.get_skill_rule("fishing", 20)
return lambda state: state.can_reach(RegionNames.Fly_Fish, "Region", self.player) and \
can_catch_fly_fish(state) and \
(state.can_reach(RegionNames.Milk, "Region", self.player) or
state.can_reach(RegionNames.Egg, "Region", self.player) or
state.can_reach(RegionNames.Shrimp, "Region", self.player) or
(state.can_reach(RegionNames.Wheat, "Region", self.player) and
state.can_reach(RegionNames.Windmill, "Region", self.player)))
if skill.lower() == "runecraft":
return lambda state: state.has(ItemNames.QP_Rune_Mysteries, self.player)
if skill.lower() == "magic":
return lambda state: state.can_reach(RegionNames.Mind_Runes, "Region", self.player)
return lambda state: True