From 9402d8240597cebd8fd4cc5d82f2de51773f6836 Mon Sep 17 00:00:00 2001 From: KonoTyran Date: Sun, 29 Aug 2021 08:30:44 -0700 Subject: [PATCH] Slay the Spire (#54) Add Slay the Spire --- worlds/spire/Items.py | 39 ++++++++++++++ worlds/spire/Locations.py | 37 +++++++++++++ worlds/spire/Options.py | 36 +++++++++++++ worlds/spire/Regions.py | 11 ++++ worlds/spire/Rules.py | 76 ++++++++++++++++++++++++++ worlds/spire/__init__.py | 110 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 309 insertions(+) create mode 100644 worlds/spire/Items.py create mode 100644 worlds/spire/Locations.py create mode 100644 worlds/spire/Options.py create mode 100644 worlds/spire/Regions.py create mode 100644 worlds/spire/Rules.py create mode 100644 worlds/spire/__init__.py diff --git a/worlds/spire/Items.py b/worlds/spire/Items.py new file mode 100644 index 00000000..c9f3e63c --- /dev/null +++ b/worlds/spire/Items.py @@ -0,0 +1,39 @@ +import typing + +from BaseClasses import Item +from typing import Dict + + +class ItemData(typing.NamedTuple): + code: typing.Optional[int] + progression: bool + event: bool = False + + +item_table: Dict[str, ItemData] = { + 'Card Draw': ItemData(8000, True), + 'Rare Card Draw': ItemData(8001, True), + 'Relic': ItemData(8002, True), + 'Boss Relic': ItemData(8003, True), + + # Event Items + 'Victory': ItemData(None, True, True), + 'Beat Act 1 Boss': ItemData(None, True, True), + 'Beat Act 2 Boss': ItemData(None, True, True), + 'Beat Act 3 Boss': ItemData(None, True, True), + +} + +item_pool: Dict[str, int] = { + 'Card Draw': 15, + 'Rare Card Draw': 3, + 'Relic': 10, + 'Boss Relic': 3 +} + +event_item_pairs: Dict[str, str] = { + "Heart Room": "Victory", + "Act 1 Boss": "Beat Act 1 Boss", + "Act 2 Boss": "Beat Act 2 Boss", + "Act 3 Boss": "Beat Act 3 Boss" +} diff --git a/worlds/spire/Locations.py b/worlds/spire/Locations.py new file mode 100644 index 00000000..6321bb3b --- /dev/null +++ b/worlds/spire/Locations.py @@ -0,0 +1,37 @@ +location_table = { + 'Card Draw 1': 19001, + 'Card Draw 2': 19002, + 'Card Draw 3': 19003, + 'Card Draw 4': 19004, + 'Card Draw 5': 19005, + 'Card Draw 6': 19006, + 'Card Draw 7': 19007, + 'Card Draw 8': 19008, + 'Card Draw 9': 19009, + 'Card Draw 10': 19010, + 'Card Draw 11': 19011, + 'Card Draw 12': 19012, + 'Card Draw 13': 19013, + 'Card Draw 14': 19014, + 'Card Draw 15': 19015, + 'Rare Card Draw 1': 21001, + 'Rare Card Draw 2': 21002, + 'Rare Card Draw 3': 21003, + 'Relic 1': 20001, + 'Relic 2': 20002, + 'Relic 3': 20003, + 'Relic 4': 20004, + 'Relic 5': 20005, + 'Relic 6': 20006, + 'Relic 7': 20007, + 'Relic 8': 20008, + 'Relic 9': 20009, + 'Relic 10': 20010, + 'Boss Relic 1': 22001, + 'Boss Relic 2': 22002, + 'Boss Relic 3': 22003, + 'Heart Room': None, + 'Act 1 Boss': None, + 'Act 2 Boss': None, + 'Act 3 Boss': None +} \ No newline at end of file diff --git a/worlds/spire/Options.py b/worlds/spire/Options.py new file mode 100644 index 00000000..ffaea7ce --- /dev/null +++ b/worlds/spire/Options.py @@ -0,0 +1,36 @@ +import typing +from Options import Choice, Option, Range, Toggle + + +class Character(Choice): + """Pick What Character you wish to play with.""" + display_name = "Character" + option_ironclad = 0 + option_silent = 1 + option_defect = 2 + option_watcher = 3 + default = 0 + + +class Ascension(Range): + """What Ascension do you wish to play with.""" + display_name = "Ascension" + range_start = 0 + range_end = 20 + default = 0 + + +class HeartRun(Toggle): + """Whether or not you will need to collect they 3 keys to unlock the final act + and beat the heart to finish the game.""" + display_name = "Heart Run" + option_true = 1 + option_false = 0 + default = 0 + + +spire_options: typing.Dict[str, type(Option)] = { + "character": Character, + "ascension": Ascension, + "heart_run": HeartRun +} \ No newline at end of file diff --git a/worlds/spire/Regions.py b/worlds/spire/Regions.py new file mode 100644 index 00000000..9e2ac0d3 --- /dev/null +++ b/worlds/spire/Regions.py @@ -0,0 +1,11 @@ +def create_regions(world, player: int): + from . import create_region + from .Locations import location_table + + world.regions += [ + create_region(world, player, 'Menu', None, ['Neow\'s Room']), + create_region(world, player, 'The Spire', [location for location in location_table]) + ] + + # link up our region with the entrance we just made + world.get_entrance('Neow\'s Room', player).connect(world.get_region('The Spire', player)) diff --git a/worlds/spire/Rules.py b/worlds/spire/Rules.py new file mode 100644 index 00000000..21dccf24 --- /dev/null +++ b/worlds/spire/Rules.py @@ -0,0 +1,76 @@ +from BaseClasses import MultiWorld +from ..AutoWorld import LogicMixin +from ..generic.Rules import set_rule + + +class SpireLogic(LogicMixin): + def _spire_has_relics(self, player: int, amount: int) -> bool: + count: int = self.item_count("Relic", player) + self.item_count("Boss Relic", player) + return count >= amount + + def _spire_has_cards(self, player: int, amount: int) -> bool: + count = self.item_count("Card Draw", player) + self.item_count("Rare Card Draw", player) + return count >= amount + + +def set_rules(world: MultiWorld, player: int): + + # Act 1 Card Draws + set_rule(world.get_location("Card Draw 1", player), lambda state: True) + set_rule(world.get_location("Card Draw 2", player), lambda state: True) + set_rule(world.get_location("Card Draw 3", player), lambda state: True) + set_rule(world.get_location("Card Draw 4", player), lambda state: state._spire_has_relics(player, 1)) + set_rule(world.get_location("Card Draw 5", player), lambda state: state._spire_has_relics(player, 1)) + + # Act 1 Relics + set_rule(world.get_location("Relic 1", player), lambda state: state._spire_has_cards(player, 1)) + set_rule(world.get_location("Relic 2", player), lambda state: state._spire_has_cards(player, 2)) + set_rule(world.get_location("Relic 3", player), lambda state: state._spire_has_cards(player, 2)) + + # Act 1 Boss Event + set_rule(world.get_location("Act 1 Boss", player), lambda state: state._spire_has_cards(player, 3) and state._spire_has_relics(player, 2)) + + # Act 1 Boss Rewards + set_rule(world.get_location("Rare Card Draw 1", player), lambda state: state.has("Beat Act 1 Boss", player)) + set_rule(world.get_location("Boss Relic 1", player), lambda state: state.has("Beat Act 1 Boss", player)) + + # Act 2 Card Draws + set_rule(world.get_location("Card Draw 6", player), lambda state: state.has("Beat Act 1 Boss", player)) + set_rule(world.get_location("Card Draw 7", player), lambda state: state.has("Beat Act 1 Boss", player)) + set_rule(world.get_location("Card Draw 8", player), lambda state: state.has("Beat Act 1 Boss", player) and state._spire_has_cards(player, 6) and state._spire_has_relics(player, 3)) + set_rule(world.get_location("Card Draw 9", player), lambda state: state.has("Beat Act 1 Boss", player) and state._spire_has_cards(player, 6) and state._spire_has_relics(player, 4)) + set_rule(world.get_location("Card Draw 10", player), lambda state: state.has("Beat Act 1 Boss", player) and state._spire_has_cards(player, 7) and state._spire_has_relics(player, 4)) + + # Act 2 Relics + set_rule(world.get_location("Relic 4", player), lambda state: state.has("Beat Act 1 Boss", player) and state._spire_has_cards(player, 7) and state._spire_has_relics(player, 2)) + set_rule(world.get_location("Relic 5", player), lambda state: state.has("Beat Act 1 Boss", player) and state._spire_has_cards(player, 7) and state._spire_has_relics(player, 2)) + set_rule(world.get_location("Relic 6", player), lambda state: state.has("Beat Act 1 Boss", player) and state._spire_has_cards(player, 7) and state._spire_has_relics(player, 3)) + + # Act 2 Boss Event + set_rule(world.get_location("Act 2 Boss", player), lambda state: state.has("Beat Act 1 Boss", player) and state._spire_has_cards(player, 7) and state._spire_has_relics(player, 4) and state.has("Boss Relic", player)) + + # Act 2 Boss Rewards + set_rule(world.get_location("Rare Card Draw 2", player), lambda state: state.has("Beat Act 2 Boss", player)) + set_rule(world.get_location("Boss Relic 2", player), lambda state: state.has("Beat Act 2 Boss", player)) + + # Act 3 Card Draws + set_rule(world.get_location("Card Draw 11", player), lambda state: state.has("Beat Act 2 Boss", player)) + set_rule(world.get_location("Card Draw 12", player), lambda state: state.has("Beat Act 2 Boss", player)) + set_rule(world.get_location("Card Draw 13", player), lambda state: state.has("Beat Act 2 Boss", player) and state._spire_has_relics(player, 4)) + set_rule(world.get_location("Card Draw 14", player), lambda state: state.has("Beat Act 2 Boss", player) and state._spire_has_relics(player, 4)) + set_rule(world.get_location("Card Draw 15", player), lambda state: state.has("Beat Act 2 Boss", player) and state._spire_has_relics(player, 4)) + + # Act 3 Relics + set_rule(world.get_location("Relic 7", player), lambda state: state.has("Beat Act 2 Boss", player) and state._spire_has_relics(player, 4)) + set_rule(world.get_location("Relic 8", player), lambda state: state.has("Beat Act 2 Boss", player) and state._spire_has_relics(player, 5)) + set_rule(world.get_location("Relic 9", player), lambda state: state.has("Beat Act 2 Boss", player) and state._spire_has_relics(player, 5)) + set_rule(world.get_location("Relic 10", player), lambda state: state.has("Beat Act 2 Boss", player) and state._spire_has_relics(player, 5)) + + # Act 3 Boss Event + set_rule(world.get_location("Act 3 Boss", player), lambda state: state.has("Beat Act 2 Boss", player) and state._spire_has_relics(player, 7) and state.has("Boss Relic", player, 2)) + + # Act 3 Boss Rewards + set_rule(world.get_location("Rare Card Draw 3", player), lambda state: state.has("Beat Act 3 Boss", player)) + set_rule(world.get_location("Boss Relic 3", player), lambda state: state.has("Beat Act 3 Boss", player)) + + set_rule(world.get_location("Heart Room", player), lambda state: state.has("Beat Act 3 Boss", player)) diff --git a/worlds/spire/__init__.py b/worlds/spire/__init__.py new file mode 100644 index 00000000..0c7ee9c9 --- /dev/null +++ b/worlds/spire/__init__.py @@ -0,0 +1,110 @@ +import string + +from BaseClasses import Item, MultiWorld, Region, Location, Entrance +from .Items import item_table, item_pool, event_item_pairs +from .Locations import location_table +from .Regions import create_regions +from .Rules import set_rules +from ..AutoWorld import World +from .Options import spire_options + + +class SpireWorld(World): + options = spire_options + game = "Slay the Spire" + topology_present = False + data_version = 1 + + item_name_to_id = {name: data.code for name, data in item_table.items()} + location_name_to_id = location_table + + def _get_slot_data(self): + return { + 'seed': "".join(self.world.slot_seeds[self.player].choice(string.ascii_letters) for i in range(16)), + 'character': self.world.character[self.player], + 'ascension': self.world.ascension[self.player], + 'heart_run': self.world.heart_run[self.player] + } + + def generate_basic(self): + # Fill out our pool with our items from item_pool, assuming 1 item if not present in item_pool + pool = [] + for name, data in item_table.items(): + if not data.event: + if name in item_pool: + card_draw = 0 + for amount in range(item_pool[name]): + item = SpireItem(name, self.player) + + # This feels wrong but it makes our failure rate drop dramatically + # makes all but 7 basic card draws trash fill + if item.name == "Card Draw": + card_draw += 1 + if card_draw > 7: + item.advancement = False + + pool.append(item) + else: + item = SpireItem(name, self.player) + pool.append(item) + + self.world.itempool += pool + + # Pair up our event locations with our event items + for event, item in event_item_pairs.items(): + event_item = SpireItem(item, self.player) + self.world.get_location(event, self.player).place_locked_item(event_item) + + if self.world.logic[self.player] != 'no logic': + self.world.completion_condition[self.player] = lambda state: state.has("Victory", self.player) + + + def set_rules(self): + set_rules(self.world, self.player) + + def create_item(self, name: str) -> Item: + item_data = item_table[name] + return Item(name, item_data.progression, item_data.code, self.player) + + def create_regions(self): + create_regions(self.world, self.player) + + def fill_slot_data(self) -> dict: + slot_data = self._get_slot_data() + for option_name in spire_options: + option = getattr(self.world, option_name)[self.player] + slot_data[option_name] = int(option.value) + return slot_data + + +def create_region(world: MultiWorld, player: int, name: str, locations=None, exits=None): + ret = Region(name, None, name, player) + ret.world = world + if locations: + for location in locations: + loc_id = location_table.get(location, 0) + location = SpireLocation(player, location, loc_id, ret) + ret.locations.append(location) + if exits: + for exit in exits: + ret.exits.append(Entrance(player, exit, ret)) + + return ret + + +class SpireLocation(Location): + game: str = "Slay the Spire" + + def __init__(self, player: int, name: str, address=None, parent=None): + super(SpireLocation, self).__init__(player, name, address, parent) + if address is None: + self.event = True + self.locked = True + + +class SpireItem(Item): + game = "Slay the Spire" + + def __init__(self, name, player: int = None): + item_data = item_table[name] + super(SpireItem, self).__init__(name, item_data.progression, item_data.code, player)