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 1 commit
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
57 changes: 30 additions & 27 deletions worlds/zillion/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import os
import logging

from typing_extensions import override

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

Expand Down Expand Up @@ -97,7 +99,7 @@ def __init__(self, logger: logging.Logger) -> None:
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 @@ -122,7 +124,7 @@ def flush(self) -> None:
slot_data_ready: threading.Event
""" This event is set in `generate_output` when the data is ready for `fill_slot_data` """

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 +135,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:
if not hasattr(self.multiworld, "zillion_logic_cache"):
setattr(self.multiworld, "zillion_logic_cache", {})
Expand All @@ -153,12 +156,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 @@ -178,13 +182,13 @@ 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])
Expand All @@ -194,7 +198,7 @@ def create_regions(self) -> None:
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 @@ -221,15 +225,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 @@ -253,36 +258,29 @@ 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"]
group_players = set(group["players"])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I has question: can a non-empty abstract set here be a frozenset?
If not, wouldn't an assert be better? You copy it and then assign it back, which seems fragile.
Alternatively group["players"] = group_players = set(group["players"]) would be an option?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it did seem a little more fragile to separate those assignments.
I changed it to group["players"] = group_players = set(group["players"])

Even if the current code might not allow the frozenset with items, that seems more like an implementation detail that I shouldn't rely on.

I generated a few seeds with item links and looked at spoilers after this change.

players_start_chars: List[Tuple[int, Chars]] = []
for player in group_players:
z_world = multiworld.worlds[player]
Expand All @@ -301,11 +299,12 @@ def stage_generate_basic(multiworld: MultiWorld, *args: Any) -> None:
for p, sc in players_start_chars:
if sc != to_stay:
group_players.remove(p)
assert "world" in group
group["players"] = group_players
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 Down Expand Up @@ -362,10 +361,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 @@ -387,6 +387,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 @@ -411,6 +412,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 @@ -431,6 +433,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"
43 changes: 23 additions & 20 deletions worlds/zillion/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from Utils import async_start

import colorama
from typing_extensions import override

from zilliandomizer.zri.memory import Memory, RescueInfo
from zilliandomizer.zri import events
Expand All @@ -35,11 +36,11 @@ def _cmd_map(self) -> None:


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):
Expand Down Expand Up @@ -119,22 +120,22 @@ def reset_game_state(self) -> None:
self.finished_game = False
self.items_received.clear()

# override
@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
Expand All @@ -157,7 +158,7 @@ class MapPanel(Widget):
_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"
Expand All @@ -183,7 +184,7 @@ def _make_numbers(self) -> None:
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:
Expand All @@ -203,6 +204,7 @@ def update_map(self, *args: Any) -> None:
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)
Expand All @@ -221,11 +223,12 @@ def set_rooms(self, rooms: List[List[int]]) -> None:
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")

@override
def on_package(self, cmd: str, args: Dict[str, Any]) -> None:
self.room_item_numbers_to_ui()
if cmd == "Connected":
Expand All @@ -238,7 +241,7 @@ def on_package(self, cmd: str, args: Dict[str, Any]) -> None:
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}")
Expand All @@ -259,7 +262,7 @@ def on_package(self, cmd: str, args: Dict[str, Any]) -> None:
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 = {}
Expand Down Expand Up @@ -321,9 +324,9 @@ def process_from_game_queue(self) -> None:
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,
Expand All @@ -334,7 +337,7 @@ def process_from_game_queue(self) -> None:
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
Expand Down Expand Up @@ -362,7 +365,7 @@ def process_items_received(self) -> None:
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)
)
Expand All @@ -374,12 +377,12 @@ def name_seed_from_ram(data: bytes) -> Tuple[str, str]:
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()
Expand Down Expand Up @@ -479,8 +482,8 @@ def log_no_spam(msg: str) -> 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)
Expand Down
2 changes: 1 addition & 1 deletion worlds/zillion/id_maps.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ def make_room_name(row: int, col: int) -> str:


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()}"
Expand Down
2 changes: 1 addition & 1 deletion worlds/zillion/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading
Loading