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:
parent
b5343a36ff
commit
2fb59d39c9
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) \
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue