Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Zillion: use "new" settings api and cleaning #3903

Merged
merged 7 commits into from
Nov 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 42 additions & 41 deletions worlds/zillion/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
import functools
import settings
import threading
import typing
from typing import Any, Dict, List, Set, Tuple, Optional, Union
from typing import Any, ClassVar
import os
import logging

from typing_extensions import override

from BaseClasses import ItemClassification, LocationProgressType, \
MultiWorld, Item, CollectionState, Entrance, Tutorial

Expand Down Expand Up @@ -76,7 +77,7 @@ class ZillionWorld(World):
options_dataclass = ZillionOptions
options: ZillionOptions # type: ignore

settings: typing.ClassVar[ZillionSettings] # type: ignore
settings: ClassVar[ZillionSettings] # type: ignore
# these type: ignore are because of this issue: https://github.com/python/typing/discussions/1486

topology_present = True # indicate if world type has any meaningful layout/pathing
Expand All @@ -89,14 +90,14 @@ class ZillionWorld(World):

class LogStreamInterface:
logger: logging.Logger
buffer: List[str]
buffer: list[str]

def __init__(self, logger: logging.Logger) -> None:
self.logger = logger
self.buffer = []

def write(self, msg: str) -> None:
if msg.endswith('\n'):
if msg.endswith("\n"):
self.buffer.append(msg[:-1])
self.logger.debug("".join(self.buffer))
self.buffer = []
Expand All @@ -108,21 +109,21 @@ def flush(self) -> None:

lsi: LogStreamInterface

id_to_zz_item: Optional[Dict[int, ZzItem]] = None
id_to_zz_item: dict[int, ZzItem] | None = None
zz_system: System
_item_counts: "Counter[str]" = Counter()
_item_counts: Counter[str] = Counter()
"""
These are the items counts that will be in the game,
which might be different from the item counts the player asked for in options
(if the player asked for something invalid).
"""
my_locations: List[ZillionLocation] = []
my_locations: list[ZillionLocation] = []
""" This is kind of a cache to avoid iterating through all the multiworld locations in logic. """
slot_data_ready: threading.Event
""" This event is set in `generate_output` when the data is ready for `fill_slot_data` """
logic_cache: Union[ZillionLogicCache, None] = None
logic_cache: ZillionLogicCache | None = None

def __init__(self, world: MultiWorld, player: int):
def __init__(self, world: MultiWorld, player: int) -> None:
super().__init__(world, player)
self.logger = logging.getLogger("Zillion")
self.lsi = ZillionWorld.LogStreamInterface(self.logger)
Expand All @@ -133,6 +134,7 @@ def _make_item_maps(self, start_char: Chars) -> None:
_id_to_name, _id_to_zz_id, id_to_zz_item = make_id_to_others(start_char)
self.id_to_zz_item = id_to_zz_item

@override
def generate_early(self) -> None:
zz_op, item_counts = validate(self.options)

Expand All @@ -150,12 +152,13 @@ def generate_early(self) -> None:
# just in case the options changed anything (I don't think they do)
assert self.zz_system.randomizer, "init failed"
for zz_name in self.zz_system.randomizer.locations:
if zz_name != 'main':
if zz_name != "main":
assert self.zz_system.randomizer.loc_name_2_pretty[zz_name] in self.location_name_to_id, \
f"{self.zz_system.randomizer.loc_name_2_pretty[zz_name]} not in location map"

self._make_item_maps(zz_op.start_char)

@override
def create_regions(self) -> None:
assert self.zz_system.randomizer, "generate_early hasn't been called"
assert self.id_to_zz_item, "generate_early hasn't been called"
Expand All @@ -177,23 +180,23 @@ def create_regions(self) -> None:
zz_loc.req.gun = 1
assert len(self.zz_system.randomizer.get_locations(Req(gun=1, jump=1))) != 0

start = self.zz_system.randomizer.regions['start']
start = self.zz_system.randomizer.regions["start"]

all: Dict[str, ZillionRegion] = {}
all_regions: dict[str, ZillionRegion] = {}
for here_zz_name, zz_r in self.zz_system.randomizer.regions.items():
here_name = "Menu" if here_zz_name == "start" else zz_reg_name_to_reg_name(here_zz_name)
all[here_name] = ZillionRegion(zz_r, here_name, here_name, p, w)
self.multiworld.regions.append(all[here_name])
all_regions[here_name] = ZillionRegion(zz_r, here_name, here_name, p, w)
self.multiworld.regions.append(all_regions[here_name])

limited_skill = Req(gun=3, jump=3, skill=self.zz_system.randomizer.options.skill, hp=940, red=1, floppy=126)
queue = deque([start])
done: Set[str] = set()
done: set[str] = set()
while len(queue):
zz_here = queue.popleft()
here_name = "Menu" if zz_here.name == "start" else zz_reg_name_to_reg_name(zz_here.name)
if here_name in done:
continue
here = all[here_name]
here = all_regions[here_name]

for zz_loc in zz_here.locations:
# if local gun reqs didn't place "keyword" item
Expand All @@ -217,15 +220,16 @@ def access_rule_wrapped(zz_loc_local: ZzLocation,
self.my_locations.append(loc)

for zz_dest in zz_here.connections.keys():
dest_name = "Menu" if zz_dest.name == 'start' else zz_reg_name_to_reg_name(zz_dest.name)
dest = all[dest_name]
exit = Entrance(p, f"{here_name} to {dest_name}", here)
here.exits.append(exit)
exit.connect(dest)
dest_name = "Menu" if zz_dest.name == "start" else zz_reg_name_to_reg_name(zz_dest.name)
dest = all_regions[dest_name]
exit_ = Entrance(p, f"{here_name} to {dest_name}", here)
here.exits.append(exit_)
exit_.connect(dest)

queue.append(zz_dest)
done.add(here.name)

@override
def create_items(self) -> None:
if not self.id_to_zz_item:
self._make_item_maps("JJ")
Expand All @@ -249,37 +253,30 @@ def create_items(self) -> None:
self.logger.debug(f"Zillion Items: {item_name} 1")
self.multiworld.itempool.append(self.create_item(item_name))

def set_rules(self) -> None:
# logic for this game is in create_regions
pass

@override
def generate_basic(self) -> None:
assert self.zz_system.randomizer, "generate_early hasn't been called"
# main location name is an alias
main_loc_name = self.zz_system.randomizer.loc_name_2_pretty[self.zz_system.randomizer.locations['main'].name]
main_loc_name = self.zz_system.randomizer.loc_name_2_pretty[self.zz_system.randomizer.locations["main"].name]

self.multiworld.get_location(main_loc_name, self.player)\
.place_locked_item(self.create_item("Win"))
self.multiworld.completion_condition[self.player] = \
lambda state: state.has("Win", self.player)

@staticmethod
def stage_generate_basic(multiworld: MultiWorld, *args: Any) -> None:
def stage_generate_basic(multiworld: MultiWorld, *args: Any) -> None: # noqa: ANN401
# item link pools are about to be created in main
# JJ can't be an item link unless all the players share the same start_char
# (The reason for this is that the JJ ZillionItem will have a different ZzItem depending
# on whether the start char is Apple or Champ, and the logic depends on that ZzItem.)
for group in multiworld.groups.values():
# TODO: remove asserts on group when we can specify which members of TypedDict are optional
assert "game" in group
if group["game"] == "Zillion":
assert "item_pool" in group
if group["game"] == "Zillion" and "item_pool" in group:
item_pool = group["item_pool"]
to_stay: Chars = "JJ"
if "JJ" in item_pool:
assert "players" in group
group_players = group["players"]
players_start_chars: List[Tuple[int, Chars]] = []
group["players"] = group_players = set(group["players"])
players_start_chars: list[tuple[int, Chars]] = []
for player in group_players:
z_world = multiworld.worlds[player]
assert isinstance(z_world, ZillionWorld)
Expand All @@ -291,17 +288,17 @@ def stage_generate_basic(multiworld: MultiWorld, *args: Any) -> None:
elif start_char_counts["Champ"] > start_char_counts["Apple"]:
to_stay = "Champ"
else: # equal
choices: Tuple[Chars, ...] = ("Apple", "Champ")
choices: tuple[Chars, ...] = ("Apple", "Champ")
to_stay = multiworld.random.choice(choices)

for p, sc in players_start_chars:
if sc != to_stay:
group_players.remove(p)
assert "world" in group
group_world = group["world"]
assert isinstance(group_world, ZillionWorld)
group_world._make_item_maps(to_stay)

@override
def post_fill(self) -> None:
"""Optional Method that is called after regular fill. Can be used to do adjustments before output generation.
This happens before progression balancing, so the items may not be in their final locations yet."""
Expand All @@ -317,10 +314,10 @@ def finalize_item_locations(self) -> GenData:

assert self.zz_system.randomizer, "generate_early hasn't been called"

# debug_zz_loc_ids: Dict[str, int] = {}
# debug_zz_loc_ids: dict[str, int] = {}
empty = zz_items[4]
multi_item = empty # a different patcher method differentiates empty from ap multi item
multi_items: Dict[str, Tuple[str, str]] = {} # zz_loc_name to (item_name, player_name)
multi_items: dict[str, tuple[str, str]] = {} # zz_loc_name to (item_name, player_name)
for z_loc in self.multiworld.get_locations(self.player):
assert isinstance(z_loc, ZillionLocation)
# debug_zz_loc_ids[z_loc.zz_loc.name] = id(z_loc.zz_loc)
Expand All @@ -343,7 +340,7 @@ def finalize_item_locations(self) -> GenData:
# print(id_)
# print("size:", len(debug_zz_loc_ids))

# debug_loc_to_id: Dict[str, int] = {}
# debug_loc_to_id: dict[str, int] = {}
# regions = self.zz_randomizer.regions
# for region in regions.values():
# for loc in region.locations:
Expand All @@ -358,10 +355,11 @@ def finalize_item_locations(self) -> GenData:
f"in world {self.player} didn't get an item"
)

game_id = self.multiworld.player_name[self.player].encode() + b'\x00' + self.multiworld.seed_name[-6:].encode()
game_id = self.multiworld.player_name[self.player].encode() + b"\x00" + self.multiworld.seed_name[-6:].encode()

return GenData(multi_items, self.zz_system.get_game(), game_id)

@override
def generate_output(self, output_directory: str) -> None:
"""This method gets called from a threadpool, do not use multiworld.random here.
If you need any last-second randomization, use self.random instead."""
Expand All @@ -383,6 +381,7 @@ def generate_output(self, output_directory: str) -> None:

self.logger.debug(f"Zillion player {self.player} finished generate_output")

@override
def fill_slot_data(self) -> ZillionSlotInfo: # json of WebHostLib.models.Slot
"""Fill in the `slot_data` field in the `Connected` network package.
This is a way the generator can give custom data to the client.
Expand All @@ -400,6 +399,7 @@ def fill_slot_data(self) -> ZillionSlotInfo: # json of WebHostLib.models.Slot

# end of ordered Main.py calls

@override
def create_item(self, name: str) -> Item:
"""Create an item for this world type and player.
Warning: this may be called with self.multiworld = None, for example by MultiServer"""
Expand All @@ -420,6 +420,7 @@ def create_item(self, name: str) -> Item:
z_item = ZillionItem(name, classification, item_id, self.player, zz_item)
return z_item

@override
def get_filler_item_name(self) -> str:
"""Called when the item pool needs to be filled with additional items to match location count."""
return "Empty"
Loading
Loading