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

* Zillion: use "new" settings api and cleaning

* python 3.10 typing update

* don't separate assignments of item link players
This commit is contained in:
Doug Hoskisson
2024-11-29 12:25:01 -08:00
committed by GitHub
parent b5343a36ff
commit 2fb59d39c9
10 changed files with 136 additions and 120 deletions

View File

@@ -3,11 +3,12 @@ from contextlib import redirect_stdout
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
@@ -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
@@ -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 = []
@@ -108,21 +109,21 @@ class ZillionWorld(World):
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)
@@ -133,6 +134,7 @@ class ZillionWorld(World):
_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)
@@ -150,12 +152,13 @@ class ZillionWorld(World):
# 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"
@@ -177,23 +180,23 @@ class ZillionWorld(World):
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
@@ -217,15 +220,16 @@ class ZillionWorld(World):
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")
@@ -249,14 +253,11 @@ class ZillionWorld(World):
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"))
@@ -264,22 +265,18 @@ class ZillionWorld(World):
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)
@@ -291,17 +288,17 @@ class ZillionWorld(World):
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."""
@@ -317,10 +314,10 @@ class ZillionWorld(World):
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)
@@ -343,7 +340,7 @@ class ZillionWorld(World):
# 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:
@@ -358,10 +355,11 @@ class ZillionWorld(World):
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."""
@@ -383,6 +381,7 @@ class ZillionWorld(World):
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.
@@ -400,6 +399,7 @@ class ZillionWorld(World):
# 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"""
@@ -420,6 +420,7 @@ class ZillionWorld(World):
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"