Initial FF1R implementation (#123)

FF1R
This commit is contained in:
jtoyoda
2021-11-28 14:32:08 -07:00
committed by GitHub
parent 7b0b243607
commit 6566dde8d0
16 changed files with 2186 additions and 2 deletions

71
worlds/ff1/Items.py Normal file
View File

@@ -0,0 +1,71 @@
import json
from pathlib import Path
from typing import Dict, Set, NamedTuple, List
from BaseClasses import Item
class ItemData(NamedTuple):
name: str
code: int
item_type: str
progression: bool
FF1_BRIDGE = 'Bridge'
FF1_STARTER_ITEMS = [
"Ship"
]
FF1_PROGRESSION_LIST = [
"Rod", "Cube", "Lute", "Key", "Chime", "Oxyale",
"Ship", "Canoe", "Floater", "Canal",
"Crown", "Crystal", "Herb", "Tnt", "Adamant", "Slab", "Ruby", "Bottle",
"Shard",
"EarthOrb", "FireOrb", "WaterOrb", "AirOrb"
]
class FF1Items:
_item_table: List[ItemData] = []
_item_table_lookup: Dict[str, ItemData] = {}
def _populate_item_table_from_data(self):
base_path = Path(__file__).parent
file_path = (base_path / "data/items.json").resolve()
with open(file_path) as file:
items = json.load(file)
# Hardcode progression and categories for now
self._item_table = [ItemData(name, code, "FF1Item", name in FF1_PROGRESSION_LIST)
for name, code in items.items()]
self._item_table_lookup = {item.name: item for item in self._item_table}
def _get_item_table(self) -> List[ItemData]:
if not self._item_table or not self._item_table_lookup:
self._populate_item_table_from_data()
return self._item_table
def _get_item_table_lookup(self) -> Dict[str, ItemData]:
if not self._item_table or not self._item_table_lookup:
self._populate_item_table_from_data()
return self._item_table_lookup
def get_item_names_per_category(self) -> Dict[str, Set[str]]:
categories: Dict[str, Set[str]] = {}
for item in self._get_item_table():
categories.setdefault(item.item_type, set()).add(item.name)
return categories
def generate_item(self, name: str, player: int) -> Item:
item = self._get_item_table_lookup().get(name)
return Item(name, item.progression, item.code, player)
def get_item_name_to_code_dict(self) -> Dict[str, int]:
return {name: item.code for name, item in self._get_item_table_lookup().items()}
def get_item(self, name: str) -> ItemData:
return self._get_item_table_lookup()[name]

75
worlds/ff1/Locations.py Normal file
View File

@@ -0,0 +1,75 @@
import json
from pathlib import Path
from typing import Dict, NamedTuple, List, Optional
from BaseClasses import Region, RegionType, Location
EventId: Optional[int] = None
CHAOS_TERMINATED_EVENT = 'Terminated Chaos'
class LocationData(NamedTuple):
name: str
address: int
class FF1Locations:
_location_table: List[LocationData] = []
_location_table_lookup: Dict[str, LocationData] = {}
def _populate_item_table_from_data(self):
base_path = Path(__file__).parent
file_path = (base_path / "data/locations.json").resolve()
with open(file_path) as file:
locations = json.load(file)
# Hardcode progression and categories for now
self._location_table = [LocationData(name, code) for name, code in locations.items()]
self._location_table_lookup = {item.name: item for item in self._location_table}
def _get_location_table(self) -> List[LocationData]:
if not self._location_table or not self._location_table_lookup:
self._populate_item_table_from_data()
return self._location_table
def _get_location_table_lookup(self) -> Dict[str, LocationData]:
if not self._location_table or not self._location_table_lookup:
self._populate_item_table_from_data()
return self._location_table_lookup
def get_location_name_to_address_dict(self) -> Dict[str, int]:
data = {name: location.address for name, location in self._get_location_table_lookup().items()}
data[CHAOS_TERMINATED_EVENT] = EventId
return data
@staticmethod
def create_menu_region(player: int, locations: Dict[str, int],
rules: Dict[str, List[List[str]]]) -> Region:
menu_region = Region("Menu", RegionType.Generic, "Menu", player)
for name, address in locations.items():
location = Location(player, name, address, menu_region)
## TODO REMOVE WHEN LOGIC FOR TOFR IS CORRECT
if "ToFR" in name:
rules_list = [["Rod", "Cube", "Lute", "Key", "Chime", "Oxyale",
"Ship", "Canoe", "Floater", "Canal",
"Crown", "Crystal", "Herb", "Tnt", "Adamant", "Slab", "Ruby", "Bottle"]]
location.access_rule = generate_rule(rules_list, player)
elif name in rules:
rules_list = rules[name]
location.access_rule = generate_rule(rules_list, player)
menu_region.locations.append(location)
return menu_region
def generate_rule(rules_list, player):
def x(state):
for rule in rules_list:
current_state = True
for item in rule:
if not state.has(item, player):
current_state = False
break
if current_state:
return True
return False
return x

22
worlds/ff1/Options.py Normal file
View File

@@ -0,0 +1,22 @@
from typing import Dict
from Options import OptionDict
class Locations(OptionDict):
displayname = "locations"
class Items(OptionDict):
displayname = "items"
class Rules(OptionDict):
displayname = "rules"
ff1_options: Dict[str, OptionDict] = {
"locations": Locations,
"items": Items,
"rules": Rules
}

97
worlds/ff1/__init__.py Normal file
View File

@@ -0,0 +1,97 @@
from typing import Dict
from BaseClasses import Item, Location, MultiWorld
from .Items import ItemData, FF1Items, FF1_STARTER_ITEMS, FF1_PROGRESSION_LIST, FF1_BRIDGE
from .Locations import EventId, FF1Locations, generate_rule, CHAOS_TERMINATED_EVENT
from .Options import ff1_options
from ..AutoWorld import World
class FF1World(World):
"""
Final Fantasy 1, originally released on the NES on 1987, is the game that started the beloved, long running series.
The randomizer takes the original 8-bit Final Fantasy game for NES (USA edition) and allows you to
shuffle important aspects like the location of key items, the difficulty of monsters and fiends,
and even the location of towns and dungeons.
Part puzzle and part speed-run, it breathes new life into one of the most influential games ever made.
"""
options = ff1_options
game = "Final Fantasy"
topology_present = False
remote_items = True
data_version = 0
remote_start_inventory = True
ff1_items = FF1Items()
ff1_locations = FF1Locations()
item_name_groups = ff1_items.get_item_names_per_category()
item_name_to_id = ff1_items.get_item_name_to_code_dict()
location_name_to_id = ff1_locations.get_location_name_to_address_dict()
def __init__(self, world: MultiWorld, player: int):
super().__init__(world, player)
self.locked_items = []
self.locked_locations = []
def generate_early(self):
return
def create_regions(self):
locations = get_options(self.world, 'locations', self.player)
rules = get_options(self.world, 'rules', self.player)
menu_region = self.ff1_locations.create_menu_region(self.player, locations, rules)
terminated_event = Location(self.player, CHAOS_TERMINATED_EVENT, EventId, menu_region)
terminated_item = Item(CHAOS_TERMINATED_EVENT, True, EventId, self.player)
terminated_event.place_locked_item(terminated_item)
items = get_options(self.world, 'items', self.player)
goal_rule = generate_rule([[name for name in items.keys() if name in FF1_PROGRESSION_LIST and name != "Shard"]],
self.player)
if "Shard" in items.keys():
def goal_rule_and_shards(state):
return goal_rule(state) and state.has("Shard", self.player, 32)
terminated_event.access_rule = goal_rule_and_shards
menu_region.locations.append(terminated_event)
self.world.regions += [menu_region]
def create_item(self, name: str) -> Item:
return self.ff1_items.generate_item(name, self.player)
def set_rules(self):
self.world.completion_condition[self.player] = lambda state: state.has(CHAOS_TERMINATED_EVENT, self.player)
def generate_basic(self):
items = get_options(self.world, 'items', self.player)
if FF1_BRIDGE in items.keys():
self._place_locked_item_in_sphere0(FF1_BRIDGE)
if items:
possible_early_items = [name for name in FF1_STARTER_ITEMS if name in items.keys()]
if possible_early_items:
progression_item = self.world.random.choice(possible_early_items)
self._place_locked_item_in_sphere0(progression_item)
items = [self.create_item(name) for name, data in items.items() for x in range(data['count']) if name not in
self.locked_items]
self.world.itempool += items
def _place_locked_item_in_sphere0(self, progression_item: str):
if progression_item:
rules = get_options(self.world, 'rules', self.player)
sphere_0_locations = [name for name, rules in rules.items()
if rules and len(rules[0]) == 0 and name not in self.locked_locations]
if sphere_0_locations:
initial_location = self.world.random.choice(sphere_0_locations)
locked_location = self.world.get_location(initial_location, self.player)
locked_location.place_locked_item(self.create_item(progression_item))
self.locked_items.append(progression_item)
self.locked_locations.append(locked_location.name)
def fill_slot_data(self) -> Dict[str, object]:
slot_data: Dict[str, object] = {}
return slot_data
def get_options(world: MultiWorld, name: str, player: int):
return getattr(world, name, None)[player].value

194
worlds/ff1/data/items.json Normal file
View File

@@ -0,0 +1,194 @@
{
"None": 256,
"Lute": 257,
"Crown": 258,
"Crystal": 259,
"Herb": 260,
"Key": 261,
"Tnt": 262,
"Adamant": 263,
"Slab": 264,
"Ruby": 265,
"Rod": 266,
"Floater": 267,
"Chime": 268,
"Tail": 269,
"Cube": 270,
"Bottle": 271,
"Oxyale": 272,
"EarthOrb": 273,
"FireOrb": 274,
"WaterOrb": 275,
"AirOrb": 276,
"Shard": 277,
"Tent": 278,
"Cabin": 279,
"House": 280,
"Heal": 281,
"Pure": 282,
"Soft": 283,
"WoodenNunchucks": 284,
"SmallKnife": 285,
"WoodenRod": 286,
"Rapier": 287,
"IronHammer": 288,
"ShortSword": 289,
"HandAxe": 290,
"Scimitar": 291,
"IronNunchucks": 292,
"LargeKnife": 293,
"IronStaff": 294,
"Sabre": 295,
"LongSword": 296,
"GreatAxe": 297,
"Falchon": 298,
"SilverKnife": 299,
"SilverSword": 300,
"SilverHammer": 301,
"SilverAxe": 302,
"FlameSword": 303,
"IceSword": 304,
"DragonSword": 305,
"GiantSword": 306,
"SunSword": 307,
"CoralSword": 308,
"WereSword": 309,
"RuneSword": 310,
"PowerRod": 311,
"LightAxe": 312,
"HealRod": 313,
"MageRod": 314,
"Defense": 315,
"WizardRod": 316,
"Vorpal": 317,
"CatClaw": 318,
"ThorHammer": 319,
"BaneSword": 320,
"Katana": 321,
"Xcalber": 322,
"Masamune": 323,
"Cloth": 324,
"WoodenArmor": 325,
"ChainArmor": 326,
"IronArmor": 327,
"SteelArmor": 328,
"SilverArmor": 329,
"FlameArmor": 330,
"IceArmor": 331,
"OpalArmor": 332,
"DragonArmor": 333,
"Copper": 334,
"Silver": 335,
"Gold": 336,
"Opal": 337,
"WhiteShirt": 338,
"BlackShirt": 339,
"WoodenShield": 340,
"IronShield": 341,
"SilverShield": 342,
"FlameShield": 343,
"IceShield": 344,
"OpalShield": 345,
"AegisShield": 346,
"Buckler": 347,
"ProCape": 348,
"Cap": 349,
"WoodenHelm": 350,
"IronHelm": 351,
"SilverHelm": 352,
"OpalHelm": 353,
"HealHelm": 354,
"Ribbon": 355,
"Gloves": 356,
"CopperGauntlets": 357,
"IronGauntlets": 358,
"SilverGauntlets": 359,
"ZeusGauntlets": 360,
"PowerGauntlets": 361,
"OpalGauntlets": 362,
"ProRing": 363,
"Gold10": 364,
"Gold20": 365,
"Gold25": 366,
"Gold30": 367,
"Gold55": 368,
"Gold70": 369,
"Gold85": 370,
"Gold110": 371,
"Gold135": 372,
"Gold155": 373,
"Gold160": 374,
"Gold180": 375,
"Gold240": 376,
"Gold255": 377,
"Gold260": 378,
"Gold295": 379,
"Gold300": 380,
"Gold315": 381,
"Gold330": 382,
"Gold350": 383,
"Gold385": 384,
"Gold400": 385,
"Gold450": 386,
"Gold500": 387,
"Gold530": 388,
"Gold575": 389,
"Gold620": 390,
"Gold680": 391,
"Gold750": 392,
"Gold795": 393,
"Gold880": 394,
"Gold1020": 395,
"Gold1250": 396,
"Gold1455": 397,
"Gold1520": 398,
"Gold1760": 399,
"Gold1975": 400,
"Gold2000": 401,
"Gold2750": 402,
"Gold3400": 403,
"Gold4150": 404,
"Gold5000": 405,
"Gold5450": 406,
"Gold6400": 407,
"Gold6720": 408,
"Gold7340": 409,
"Gold7690": 410,
"Gold7900": 411,
"Gold8135": 412,
"Gold9000": 413,
"Gold9300": 414,
"Gold9500": 415,
"Gold9900": 416,
"Gold10000": 417,
"Gold12350": 418,
"Gold13000": 419,
"Gold13450": 420,
"Gold14050": 421,
"Gold14720": 422,
"Gold15000": 423,
"Gold17490": 424,
"Gold18010": 425,
"Gold19990": 426,
"Gold20000": 427,
"Gold20010": 428,
"Gold26000": 429,
"Gold45000": 430,
"Gold65000": 431,
"Smoke": 435,
"FullCure": 432,
"Blast": 434,
"Phoenix": 433,
"Flare": 437,
"Black": 438,
"Refresh": 436,
"Guard": 439,
"Wizard": 442,
"HighPotion": 441,
"Cloak": 443,
"Quick": 440,
"Ship": 480,
"Bridge": 488,
"Canal": 492,
"Canoe": 498
}

View File

@@ -0,0 +1,257 @@
{
"Coneria1": 257,
"Coneria2": 258,
"ConeriaMajor": 259,
"Coneria4": 260,
"Coneria5": 261,
"Coneria6": 262,
"MatoyasCave1": 299,
"MatoyasCave3": 301,
"MatoyasCave2": 300,
"NorthwestCastle1": 273,
"NorthwestCastle3": 275,
"NorthwestCastle2": 274,
"ToFTopLeft1": 263,
"ToFBottomLeft": 265,
"ToFTopLeft2": 264,
"ToFRevisited6": 509,
"ToFRevisited4": 507,
"ToFRMasmune": 504,
"ToFRevisited5": 508,
"ToFRevisited3": 506,
"ToFRevisited2": 505,
"ToFRevisited7": 510,
"ToFTopRight1": 267,
"ToFTopRight2": 268,
"ToFBottomRight": 266,
"IceCave15": 377,
"IceCave16": 378,
"IceCave9": 371,
"IceCave11": 373,
"IceCave10": 372,
"IceCave12": 374,
"IceCave13": 375,
"IceCave14": 376,
"IceCave1": 363,
"IceCave2": 364,
"IceCave3": 365,
"IceCave4": 366,
"IceCave5": 367,
"IceCaveMajor": 370,
"IceCave7": 369,
"IceCave6": 368,
"Elfland1": 269,
"Elfland2": 270,
"Elfland3": 271,
"Elfland4": 272,
"Ordeals5": 383,
"Ordeals6": 384,
"Ordeals7": 385,
"Ordeals1": 379,
"Ordeals2": 380,
"Ordeals3": 381,
"Ordeals4": 382,
"OrdealsMajor": 387,
"Ordeals8": 386,
"SeaShrine7": 411,
"SeaShrine8": 412,
"SeaShrine9": 413,
"SeaShrine10": 414,
"SeaShrine1": 405,
"SeaShrine2": 406,
"SeaShrine3": 407,
"SeaShrine4": 408,
"SeaShrine5": 409,
"SeaShrine6": 410,
"SeaShrine13": 417,
"SeaShrine14": 418,
"SeaShrine11": 415,
"SeaShrine15": 419,
"SeaShrine16": 420,
"SeaShrineLocked": 421,
"SeaShrine18": 422,
"SeaShrine19": 423,
"SeaShrine20": 424,
"SeaShrine23": 427,
"SeaShrine21": 425,
"SeaShrine22": 426,
"SeaShrine24": 428,
"SeaShrine26": 430,
"SeaShrine28": 432,
"SeaShrine25": 429,
"SeaShrine30": 434,
"SeaShrine31": 435,
"SeaShrine27": 431,
"SeaShrine29": 433,
"SeaShrineMajor": 436,
"SeaShrine12": 416,
"DwarfCave3": 291,
"DwarfCave4": 292,
"DwarfCave6": 294,
"DwarfCave7": 295,
"DwarfCave5": 293,
"DwarfCave8": 296,
"DwarfCave9": 297,
"DwarfCave10": 298,
"DwarfCave1": 289,
"DwarfCave2": 290,
"Waterfall1": 437,
"Waterfall2": 438,
"Waterfall3": 439,
"Waterfall4": 440,
"Waterfall5": 441,
"Waterfall6": 442,
"MirageTower5": 456,
"MirageTower16": 467,
"MirageTower17": 468,
"MirageTower15": 466,
"MirageTower18": 469,
"MirageTower14": 465,
"SkyPalace1": 470,
"SkyPalace2": 471,
"SkyPalace3": 472,
"SkyPalace4": 473,
"SkyPalace18": 487,
"SkyPalace19": 488,
"SkyPalace16": 485,
"SkyPalaceMajor": 489,
"SkyPalace17": 486,
"SkyPalace22": 491,
"SkyPalace21": 490,
"SkyPalace23": 492,
"SkyPalace24": 493,
"SkyPalace31": 500,
"SkyPalace32": 501,
"SkyPalace33": 502,
"SkyPalace34": 503,
"SkyPalace29": 498,
"SkyPalace26": 495,
"SkyPalace25": 494,
"SkyPalace28": 497,
"SkyPalace27": 496,
"SkyPalace30": 499,
"SkyPalace14": 483,
"SkyPalace11": 480,
"SkyPalace12": 481,
"SkyPalace13": 482,
"SkyPalace15": 484,
"SkyPalace10": 479,
"SkyPalace5": 474,
"SkyPalace6": 475,
"SkyPalace7": 476,
"SkyPalace8": 477,
"SkyPalace9": 478,
"MirageTower9": 460,
"MirageTower13": 464,
"MirageTower10": 461,
"MirageTower12": 463,
"MirageTower11": 462,
"MirageTower1": 452,
"MirageTower2": 453,
"MirageTower4": 455,
"MirageTower3": 454,
"MirageTower8": 459,
"MirageTower7": 458,
"MirageTower6": 457,
"Volcano30": 359,
"Volcano32": 361,
"Volcano31": 360,
"Volcano28": 357,
"Volcano29": 358,
"Volcano21": 350,
"Volcano20": 349,
"Volcano24": 353,
"Volcano19": 348,
"Volcano25": 354,
"VolcanoMajor": 362,
"Volcano26": 355,
"Volcano27": 356,
"Volcano22": 351,
"Volcano23": 352,
"Volcano1": 330,
"Volcano9": 338,
"Volcano2": 331,
"Volcano10": 339,
"Volcano3": 332,
"Volcano8": 337,
"Volcano4": 333,
"Volcano13": 342,
"Volcano11": 340,
"Volcano7": 336,
"Volcano6": 335,
"Volcano5": 334,
"Volcano14": 343,
"Volcano12": 341,
"Volcano15": 344,
"Volcano18": 347,
"Volcano17": 346,
"Volcano16": 345,
"MarshCave6": 281,
"MarshCave5": 280,
"MarshCave7": 282,
"MarshCave8": 283,
"MarshCave10": 285,
"MarshCave2": 277,
"MarshCave11": 286,
"MarshCave3": 278,
"MarshCaveMajor": 284,
"MarshCave12": 287,
"MarshCave4": 279,
"MarshCave1": 276,
"MarshCave13": 288,
"TitansTunnel1": 326,
"TitansTunnel2": 327,
"TitansTunnel3": 328,
"TitansTunnel4": 329,
"EarthCave1": 302,
"EarthCave2": 303,
"EarthCave5": 306,
"EarthCave3": 304,
"EarthCave4": 305,
"EarthCave9": 310,
"EarthCave10": 311,
"EarthCave11": 312,
"EarthCave6": 307,
"EarthCave7": 308,
"EarthCave12": 313,
"EarthCaveMajor": 317,
"EarthCave19": 320,
"EarthCave17": 318,
"EarthCave18": 319,
"EarthCave20": 321,
"EarthCave24": 325,
"EarthCave21": 322,
"EarthCave22": 323,
"EarthCave23": 324,
"EarthCave13": 314,
"EarthCave15": 316,
"EarthCave14": 315,
"EarthCave8": 309,
"Cardia11": 398,
"Cardia9": 396,
"Cardia10": 397,
"Cardia6": 393,
"Cardia8": 395,
"Cardia7": 394,
"Cardia13": 400,
"Cardia12": 399,
"Cardia4": 391,
"Cardia5": 392,
"Cardia3": 390,
"Cardia1": 388,
"Cardia2": 389,
"CaravanShop": 767,
"King": 513,
"Princess2": 530,
"Matoya": 522,
"Astos": 519,
"Bikke": 516,
"CanoeSage": 533,
"ElfPrince": 518,
"Nerrick": 520,
"Smith": 521,
"CubeBot": 529,
"Sarda": 525,
"Fairy": 531,
"Lefein": 527
}