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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
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"

View File

@ -3,7 +3,7 @@ import base64
import io
import pkgutil
import platform
from typing import Any, ClassVar, Coroutine, Dict, List, Optional, Protocol, Tuple, cast
from typing import Any, ClassVar, Coroutine, Protocol, cast
from CommonClient import CommonContext, server_loop, gui_enabled, \
ClientCommandProcessor, logger, get_base_parser
@ -11,6 +11,7 @@ from NetUtils import ClientStatus
from Utils import async_start
import colorama
from typing_extensions import override
from zilliandomizer.zri.memory import Memory, RescueInfo
from zilliandomizer.zri import events
@ -35,11 +36,11 @@ class ZillionCommandProcessor(ClientCommandProcessor):
class ToggleCallback(Protocol):
def __call__(self) -> None: ...
def __call__(self) -> object: ...
class SetRoomCallback(Protocol):
def __call__(self, rooms: List[List[int]]) -> None: ...
def __call__(self, rooms: list[list[int]]) -> object: ...
class ZillionContext(CommonContext):
@ -47,7 +48,7 @@ class ZillionContext(CommonContext):
command_processor = ZillionCommandProcessor
items_handling = 1 # receive items from other players
known_name: Optional[str]
known_name: str | None
""" This is almost the same as `auth` except `auth` is reset to `None` when server disconnects, and this isn't. """
from_game: "asyncio.Queue[events.EventFromGame]"
@ -56,11 +57,11 @@ class ZillionContext(CommonContext):
""" local checks watched by server """
next_item: int
""" index in `items_received` """
ap_id_to_name: Dict[int, str]
ap_id_to_zz_id: Dict[int, int]
ap_id_to_name: dict[int, str]
ap_id_to_zz_id: dict[int, int]
start_char: Chars = "JJ"
rescues: Dict[int, RescueInfo] = {}
loc_mem_to_id: Dict[int, int] = {}
rescues: dict[int, RescueInfo] = {}
loc_mem_to_id: dict[int, int] = {}
got_room_info: asyncio.Event
""" flag for connected to server """
got_slot_data: asyncio.Event
@ -119,22 +120,22 @@ class ZillionContext(CommonContext):
self.finished_game = False
self.items_received.clear()
# override
def on_deathlink(self, data: Dict[str, Any]) -> None:
@override
def on_deathlink(self, data: dict[str, Any]) -> None:
self.to_game.put_nowait(events.DeathEventToGame())
return super().on_deathlink(data)
# override
@override
async def server_auth(self, password_requested: bool = False) -> None:
if password_requested and not self.password:
await super().server_auth(password_requested)
if not self.auth:
logger.info('waiting for connection to game...')
logger.info("waiting for connection to game...")
return
logger.info("logging in to server...")
await self.send_connect()
# override
@override
def run_gui(self) -> None:
from kvui import GameManager
from kivy.core.text import Label as CoreLabel
@ -154,10 +155,10 @@ class ZillionContext(CommonContext):
MAP_WIDTH: ClassVar[int] = 281
map_background: CoreImage
_number_textures: List[Texture] = []
rooms: List[List[int]] = []
_number_textures: list[Texture] = []
rooms: list[list[int]] = []
def __init__(self, **kwargs: Any) -> None:
def __init__(self, **kwargs: Any) -> None: # noqa: ANN401
super().__init__(**kwargs)
FILE_NAME = "empty-zillion-map-row-col-labels-281.png"
@ -183,7 +184,7 @@ class ZillionContext(CommonContext):
label.refresh()
self._number_textures.append(label.texture)
def update_map(self, *args: Any) -> None:
def update_map(self, *args: Any) -> None: # noqa: ANN401
self.canvas.clear()
with self.canvas:
@ -203,6 +204,7 @@ class ZillionContext(CommonContext):
num_texture = self._number_textures[num]
Rectangle(texture=num_texture, size=num_texture.size, pos=pos)
@override
def build(self) -> Layout:
container = super().build()
self.map_widget = ZillionManager.MapPanel(size_hint_x=None, width=ZillionManager.MapPanel.MAP_WIDTH)
@ -216,17 +218,18 @@ class ZillionContext(CommonContext):
self.map_widget.width = 0
self.container.do_layout()
def set_rooms(self, rooms: List[List[int]]) -> None:
def set_rooms(self, rooms: list[list[int]]) -> None:
self.map_widget.rooms = rooms
self.map_widget.update_map()
self.ui = ZillionManager(self)
self.ui_toggle_map = lambda: self.ui.toggle_map_width()
self.ui_set_rooms = lambda rooms: self.ui.set_rooms(rooms)
self.ui_toggle_map = lambda: isinstance(self.ui, ZillionManager) and self.ui.toggle_map_width()
self.ui_set_rooms = lambda rooms: isinstance(self.ui, ZillionManager) and self.ui.set_rooms(rooms)
run_co: Coroutine[Any, Any, None] = self.ui.async_run()
self.ui_task = asyncio.create_task(run_co, name="UI")
def on_package(self, cmd: str, args: Dict[str, Any]) -> None:
@override
def on_package(self, cmd: str, args: dict[str, Any]) -> None:
self.room_item_numbers_to_ui()
if cmd == "Connected":
logger.info("logged in to Archipelago server")
@ -238,7 +241,7 @@ class ZillionContext(CommonContext):
if "start_char" not in slot_data:
logger.warning("invalid Zillion `Connected` packet, `slot_data` missing `start_char`")
return
self.start_char = slot_data['start_char']
self.start_char = slot_data["start_char"]
if self.start_char not in {"Apple", "Champ", "JJ"}:
logger.warning("invalid Zillion `Connected` packet, "
f"`slot_data` `start_char` has invalid value: {self.start_char}")
@ -259,7 +262,7 @@ class ZillionContext(CommonContext):
self.rescues[0 if rescue_id == "0" else 1] = ri
if "loc_mem_to_id" not in slot_data:
logger.warn("invalid Zillion `Connected` packet, `slot_data` missing `loc_mem_to_id`")
logger.warning("invalid Zillion `Connected` packet, `slot_data` missing `loc_mem_to_id`")
return
loc_mem_to_id = slot_data["loc_mem_to_id"]
self.loc_mem_to_id = {}
@ -286,7 +289,7 @@ class ZillionContext(CommonContext):
if "keys" not in args:
logger.warning(f"invalid Retrieved packet to ZillionClient: {args}")
return
keys = cast(Dict[str, Optional[str]], args["keys"])
keys = cast(dict[str, str | None], args["keys"])
doors_b64 = keys.get(f"zillion-{self.auth}-doors", None)
if doors_b64:
logger.info("received door data from server")
@ -321,9 +324,9 @@ class ZillionContext(CommonContext):
if server_id in self.missing_locations:
self.ap_local_count += 1
n_locations = len(self.missing_locations) + len(self.checked_locations) - 1 # -1 to ignore win
logger.info(f'New Check: {loc_name} ({self.ap_local_count}/{n_locations})')
logger.info(f"New Check: {loc_name} ({self.ap_local_count}/{n_locations})")
async_start(self.send_msgs([
{"cmd": 'LocationChecks', "locations": [server_id]}
{"cmd": "LocationChecks", "locations": [server_id]}
]))
else:
# This will happen a lot in Zillion,
@ -334,7 +337,7 @@ class ZillionContext(CommonContext):
elif isinstance(event_from_game, events.WinEventFromGame):
if not self.finished_game:
async_start(self.send_msgs([
{"cmd": 'LocationChecks', "locations": [loc_name_to_id["J-6 bottom far left"]]},
{"cmd": "LocationChecks", "locations": [loc_name_to_id["J-6 bottom far left"]]},
{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}
]))
self.finished_game = True
@ -362,24 +365,24 @@ class ZillionContext(CommonContext):
ap_id = self.items_received[index].item
from_name = self.player_names[self.items_received[index].player]
# TODO: colors in this text, like sni client?
logger.info(f'received {self.ap_id_to_name[ap_id]} from {from_name}')
logger.info(f"received {self.ap_id_to_name[ap_id]} from {from_name}")
self.to_game.put_nowait(
events.ItemEventToGame(zz_item_ids)
)
self.next_item = len(self.items_received)
def name_seed_from_ram(data: bytes) -> Tuple[str, str]:
def name_seed_from_ram(data: bytes) -> tuple[str, str]:
""" returns player name, and end of seed string """
if len(data) == 0:
# no connection to game
return "", "xxx"
null_index = data.find(b'\x00')
null_index = data.find(b"\x00")
if null_index == -1:
logger.warning(f"invalid game id in rom {repr(data)}")
null_index = len(data)
name = data[:null_index].decode()
null_index_2 = data.find(b'\x00', null_index + 1)
null_index_2 = data.find(b"\x00", null_index + 1)
if null_index_2 == -1:
null_index_2 = len(data)
seed_name = data[null_index + 1:null_index_2].decode()
@ -479,8 +482,8 @@ async def zillion_sync_task(ctx: ZillionContext) -> None:
async def main() -> None:
parser = get_base_parser()
parser.add_argument('diff_file', default="", type=str, nargs="?",
help='Path to a .apzl Archipelago Binary Patch file')
parser.add_argument("diff_file", default="", type=str, nargs="?",
help="Path to a .apzl Archipelago Binary Patch file")
# SNI parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical'])
args = parser.parse_args()
print(args)

View File

@ -1,6 +1,5 @@
from dataclasses import dataclass
import json
from typing import Dict, Tuple
from zilliandomizer.game import Game as ZzGame
@ -9,7 +8,7 @@ from zilliandomizer.game import Game as ZzGame
class GenData:
""" data passed from generation to patcher """
multi_items: Dict[str, Tuple[str, str]]
multi_items: dict[str, tuple[str, str]]
""" zz_loc_name to (item_name, player_name) """
zz_game: ZzGame
game_id: bytes

View File

@ -1,5 +1,6 @@
from collections import defaultdict
from typing import Dict, Iterable, Mapping, Tuple, TypedDict
from collections.abc import Iterable, Mapping
from typing import TypedDict
from zilliandomizer.logic_components.items import (
Item as ZzItem,
@ -40,13 +41,13 @@ _zz_rescue_1 = zz_item_name_to_zz_item["rescue_1"]
_zz_empty = zz_item_name_to_zz_item["empty"]
def make_id_to_others(start_char: Chars) -> Tuple[
Dict[int, str], Dict[int, int], Dict[int, ZzItem]
def make_id_to_others(start_char: Chars) -> tuple[
dict[int, str], dict[int, int], dict[int, ZzItem]
]:
""" returns id_to_name, id_to_zz_id, id_to_zz_item """
id_to_name: Dict[int, str] = {}
id_to_zz_id: Dict[int, int] = {}
id_to_zz_item: Dict[int, ZzItem] = {}
id_to_name: dict[int, str] = {}
id_to_zz_id: dict[int, int] = {}
id_to_zz_item: dict[int, ZzItem] = {}
if start_char == "JJ":
name_to_zz_item = {
@ -91,14 +92,14 @@ def make_room_name(row: int, col: int) -> str:
return f"{chr(ord('A') + row - 1)}-{col + 1}"
loc_name_to_id: Dict[str, int] = {
loc_name_to_id: dict[str, int] = {
name: id_ + base_id
for name, id_ in pretty_loc_name_to_id.items()
}
def zz_reg_name_to_reg_name(zz_reg_name: str) -> str:
if zz_reg_name[0] == 'r' and zz_reg_name[3] == 'c':
if zz_reg_name[0] == "r" and zz_reg_name[3] == "c":
row, col = parse_reg_name(zz_reg_name)
end = zz_reg_name[5:]
return f"{make_room_name(row, col)} {end.upper()}"
@ -113,17 +114,17 @@ class ClientRescue(TypedDict):
class ZillionSlotInfo(TypedDict):
start_char: Chars
rescues: Dict[str, ClientRescue]
loc_mem_to_id: Dict[int, int]
rescues: dict[str, ClientRescue]
loc_mem_to_id: dict[int, int]
""" memory location of canister to Archipelago location id number """
def get_slot_info(regions: Iterable[RegionData],
start_char: Chars,
loc_name_to_pretty: Mapping[str, str]) -> ZillionSlotInfo:
items_placed_in_map_index: Dict[int, int] = defaultdict(int)
rescue_locations: Dict[int, RescueInfo] = {}
loc_memory_to_loc_id: Dict[int, int] = {}
items_placed_in_map_index: dict[int, int] = defaultdict(int)
rescue_locations: dict[int, RescueInfo] = {}
loc_memory_to_loc_id: dict[int, int] = {}
for region in regions:
for loc in region.locations:
assert loc.item, ("There should be an item placed in every location before "
@ -142,7 +143,7 @@ def get_slot_info(regions: Iterable[RegionData],
loc_memory_to_loc_id[loc_memory] = pretty_loc_name_to_id[loc_name_to_pretty[loc.name]]
items_placed_in_map_index[map_index] += 1
rescues: Dict[str, ClientRescue] = {}
rescues: dict[str, ClientRescue] = {}
for i in (0, 1):
if i in rescue_locations:
ri = rescue_locations[i]

View File

@ -1,4 +1,5 @@
from typing import Dict, FrozenSet, Mapping, Tuple, List, Counter as _Counter
from collections import Counter
from collections.abc import Mapping
from BaseClasses import CollectionState
@ -35,7 +36,7 @@ def set_randomizer_locs(cs: CollectionState, p: int, zz_r: Randomizer) -> int:
return _hash
def item_counts(cs: CollectionState, p: int) -> Tuple[Tuple[str, int], ...]:
def item_counts(cs: CollectionState, p: int) -> tuple[tuple[str, int], ...]:
"""
the zilliandomizer items that player p has collected
@ -44,11 +45,11 @@ def item_counts(cs: CollectionState, p: int) -> Tuple[Tuple[str, int], ...]:
return tuple((item_name, cs.count(item_name, p)) for item_name in item_name_to_id)
_cache_miss: Tuple[None, FrozenSet[Location]] = (None, frozenset())
_cache_miss: tuple[None, frozenset[Location]] = (None, frozenset())
class ZillionLogicCache:
_cache: Dict[int, Tuple[_Counter[str], FrozenSet[Location]]]
_cache: dict[int, tuple[Counter[str], frozenset[Location]]]
""" `{ hash: (counter_from_prog_items, accessible_zz_locations) }` """
_player: int
_zz_r: Randomizer
@ -60,7 +61,7 @@ class ZillionLogicCache:
self._zz_r = zz_r
self._id_to_zz_item = id_to_zz_item
def cs_to_zz_locs(self, cs: CollectionState) -> FrozenSet[Location]:
def cs_to_zz_locs(self, cs: CollectionState) -> frozenset[Location]:
"""
given an Archipelago `CollectionState`,
returns frozenset of accessible zilliandomizer locations
@ -76,7 +77,7 @@ class ZillionLogicCache:
return locs
# print("cache miss")
have_items: List[Item] = []
have_items: list[Item] = []
for name, count in counts:
have_items.extend([self._id_to_zz_item[item_name_to_id[name]]] * count)
# have_req is the result of converting AP CollectionState to zilliandomizer collection state

View File

@ -1,6 +1,6 @@
from collections import Counter
from dataclasses import dataclass
from typing import ClassVar, Dict, Literal, Tuple, TypeGuard
from typing import ClassVar, Literal, TypeGuard
from Options import Choice, DefaultOnToggle, NamedRange, OptionGroup, PerGameCommonOptions, Range, Removed, Toggle
@ -107,7 +107,7 @@ class ZillionStartChar(Choice):
display_name = "start character"
default = "random"
_name_capitalization: ClassVar[Dict[int, Chars]] = {
_name_capitalization: ClassVar[dict[int, Chars]] = {
option_jj: "JJ",
option_apple: "Apple",
option_champ: "Champ",
@ -263,7 +263,7 @@ class ZillionMapGen(Choice):
option_full = 2
default = 0
def zz_value(self) -> Literal['none', 'rooms', 'full']:
def zz_value(self) -> Literal["none", "rooms", "full"]:
if self.value == ZillionMapGen.option_none:
return "none"
if self.value == ZillionMapGen.option_rooms:
@ -305,7 +305,7 @@ z_option_groups = [
]
def convert_item_counts(ic: "Counter[str]") -> ZzItemCounts:
def convert_item_counts(ic: Counter[str]) -> ZzItemCounts:
tr: ZzItemCounts = {
ID.card: ic["ID Card"],
ID.red: ic["Red ID Card"],
@ -319,7 +319,7 @@ def convert_item_counts(ic: "Counter[str]") -> ZzItemCounts:
return tr
def validate(options: ZillionOptions) -> "Tuple[ZzOptions, Counter[str]]":
def validate(options: ZillionOptions) -> tuple[ZzOptions, Counter[str]]:
"""
adjusts options to make game completion possible

View File

@ -1,5 +1,5 @@
import os
from typing import Any, BinaryIO, Optional, cast
from typing import BinaryIO
import zipfile
from typing_extensions import override
@ -11,11 +11,11 @@ from zilliandomizer.patch import Patcher
from .gen_data import GenData
USHASH = 'd4bf9e7bcf9a48da53785d2ae7bc4270'
US_HASH = "d4bf9e7bcf9a48da53785d2ae7bc4270"
class ZillionPatch(APAutoPatchInterface):
hash = USHASH
hash = US_HASH
game = "Zillion"
patch_file_ending = ".apzl"
result_file_ending = ".sms"
@ -23,8 +23,14 @@ class ZillionPatch(APAutoPatchInterface):
gen_data_str: str
""" JSON encoded """
def __init__(self, *args: Any, gen_data_str: str = "", **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
def __init__(self,
path: str | None = None,
player: int | None = None,
player_name: str = "",
server: str = "",
*,
gen_data_str: str = "") -> None:
super().__init__(path=path, player=player, player_name=player_name, server=server)
self.gen_data_str = gen_data_str
@classmethod
@ -44,15 +50,17 @@ class ZillionPatch(APAutoPatchInterface):
super().read_contents(opened_zipfile)
self.gen_data_str = opened_zipfile.read("gen_data.json").decode()
@override
def patch(self, target: str) -> None:
self.read()
write_rom_from_gen_data(self.gen_data_str, target)
def get_base_rom_path(file_name: Optional[str] = None) -> str:
options = Utils.get_options()
def get_base_rom_path(file_name: str | None = None) -> str:
from . import ZillionSettings, ZillionWorld
settings: ZillionSettings = ZillionWorld.settings
if not file_name:
file_name = cast(str, options["zillion_options"]["rom_file"])
file_name = settings.rom_file
if not os.path.exists(file_name):
file_name = Utils.user_path(file_name)
return file_name

View File

@ -1,9 +1,11 @@
from typing import Optional
from BaseClasses import MultiWorld, Region, Location, Item, CollectionState
from typing_extensions import override
from zilliandomizer.logic_components.regions import Region as ZzRegion
from zilliandomizer.logic_components.locations import Location as ZzLocation
from zilliandomizer.logic_components.items import RESCUE
from BaseClasses import MultiWorld, Region, Location, Item, CollectionState
from .id_maps import loc_name_to_id
from .item import ZillionItem
@ -28,12 +30,12 @@ class ZillionLocation(Location):
zz_loc: ZzLocation,
player: int,
name: str,
parent: Optional[Region] = None) -> None:
parent: Region | None = None) -> None:
loc_id = loc_name_to_id[name]
super().__init__(player, name, loc_id, parent)
self.zz_loc = zz_loc
# override
@override
def can_fill(self, state: CollectionState, item: Item, check_access: bool = True) -> bool:
saved_gun_req = -1
if isinstance(item, ZillionItem) \

View File

@ -1,4 +1,3 @@
from typing import cast
from . import ZillionTestBase
from .. import ZillionWorld
@ -9,7 +8,8 @@ class SeedTest(ZillionTestBase):
def test_reproduce_seed(self) -> None:
self.world_setup(42)
z_world = cast(ZillionWorld, self.multiworld.worlds[1])
z_world = self.multiworld.worlds[1]
assert isinstance(z_world, ZillionWorld)
r = z_world.zz_system.randomizer
assert r
randomized_requirements_first = tuple(
@ -18,7 +18,8 @@ class SeedTest(ZillionTestBase):
)
self.world_setup(42)
z_world = cast(ZillionWorld, self.multiworld.worlds[1])
z_world = self.multiworld.worlds[1]
assert isinstance(z_world, ZillionWorld)
r = z_world.zz_system.randomizer
assert r
randomized_requirements_second = tuple(

View File

@ -1,4 +1,3 @@
from typing import cast
from test.bases import WorldTestBase
from .. import ZillionWorld
@ -13,8 +12,9 @@ class ZillionTestBase(WorldTestBase):
This makes sure that gun 3 is required by making all the canisters
in O-7 (including key word canisters) require gun 3.
"""
zz_world = cast(ZillionWorld, self.multiworld.worlds[1])
assert zz_world.zz_system.randomizer
for zz_loc_name, zz_loc in zz_world.zz_system.randomizer.locations.items():
z_world = self.multiworld.worlds[1]
assert isinstance(z_world, ZillionWorld)
assert z_world.zz_system.randomizer
for zz_loc_name, zz_loc in z_world.zz_system.randomizer.locations.items():
if zz_loc_name.startswith("r15c6"):
zz_loc.req.gun = 3