Merge branch 'main' into breaking_changes

# Conflicts:
#	Main.py
#	Utils.py
This commit is contained in:
Fabian Dill 2021-03-07 22:04:06 +01:00
commit f7dc21ddcc
9 changed files with 142 additions and 72 deletions

15
Main.py
View File

@ -39,11 +39,12 @@ def get_seed(seed=None):
return seed
seeds: Dict[tuple, str] = dict()
def get_same_seed(world: MultiWorld, seed_def: tuple) -> str:
seeds: Dict[tuple, str] = getattr(world, "__named_seeds", {})
if seed_def in seeds:
return seeds[seed_def]
seeds[seed_def] = str(world.random.randint(0, 2 ** 64))
world.__named_seeds = seeds
return seeds[seed_def]
@ -121,9 +122,15 @@ def main(args, seed=None):
world.shuffle[player] = shuffle
if shuffle == "vanilla":
world.er_seeds[player] = "vanilla"
elif seed.startswith("team-"):
elif seed.startswith("group-"): # renamed from team to group to not confuse with existing team name use
world.er_seeds[player] = get_same_seed(world, (shuffle, seed, world.retro[player], world.mode[player], world.logic[player]))
else:
elif seed.startswith("team-"): # TODO: remove on breaking_changes
world.er_seeds[player] = get_same_seed(world, (shuffle, seed, world.retro[player], world.mode[player], world.logic[player]))
elif not args.race:
world.er_seeds[player] = seed
elif seed: # race but with a set seed, ignore set seed and use group logic instead
world.er_seeds[player] = get_same_seed(world, (shuffle, seed, world.retro[player], world.mode[player], world.logic[player]))
else: # race but without a set seed
world.er_seeds[player] = seed
elif world.shuffle[player] == "vanilla":
world.er_seeds[player] = "vanilla"
@ -307,7 +314,7 @@ def main(args, seed=None):
apply_rom_settings(rom, args.heartbeep[player], args.heartcolor[player], args.quickswap[player],
args.fastmenu[player], args.disablemusic[player], args.sprite[player],
palettes_options, world, player, True,
reduceflashing=args.reduceflashing[player] if not args.race else True,
reduceflashing=args.reduceflashing[player] or args.race,
triforcehud=args.triforcehud[player])
mcsb_name = ''

View File

@ -294,6 +294,8 @@ def prefer_int(input_data: str) -> typing.Union[str, int]:
available_boss_names: typing.Set[str] = {boss.lower() for boss in Bosses.boss_table if boss not in
{'Agahnim', 'Agahnim2', 'Ganon'}}
available_boss_locations: typing.Set[str] = {f"{loc.lower()}{f' {level}' if level else ''}" for loc, level in
Bosses.boss_location_table}
boss_shuffle_options = {None: 'none',
'none': 'none',
@ -370,6 +372,42 @@ def roll_triggers(weights: dict) -> dict:
f"Please fix your triggers.") from e
return weights
def get_plando_bosses(boss_shuffle: str, plando_options: typing.Set[str]) -> str:
if boss_shuffle in boss_shuffle_options:
return boss_shuffle_options[boss_shuffle]
elif "bosses" in plando_options:
options = boss_shuffle.lower().split(";")
remainder_shuffle = "none" # vanilla
bosses = []
for boss in options:
if boss in boss_shuffle_options:
remainder_shuffle = boss_shuffle_options[boss]
elif "-" in boss:
loc, boss_name = boss.split("-")
if boss_name not in available_boss_names:
raise ValueError(f"Unknown Boss name {boss_name}")
if loc not in available_boss_locations:
raise ValueError(f"Unknown Boss Location {loc}")
level = ''
if loc.split(" ")[-1] in {"top", "middle", "bottom"}:
# split off level
loc = loc.split(" ")
level = f" {loc[-1]}"
loc = " ".join(loc[:-1])
loc = loc.title().replace("Of", "of")
if not Bosses.can_place_boss(boss_name.title(), loc, level):
raise ValueError(f"Cannot place {boss_name} at {loc}{level}")
bosses.append(boss)
elif boss not in available_boss_names:
raise ValueError(f"Unknown Boss name or Boss shuffle option {boss}.")
else:
bosses.append(boss)
return ";".join(bosses + [remainder_shuffle])
else:
raise Exception(f"Boss Shuffle {boss_shuffle} is unknown and boss plando is turned off.")
def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("bosses"))):
if "pre_rolled" in weights:
pre_rolled = weights["pre_rolled"]
@ -380,10 +418,25 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
item["world"],
item["from_pool"],
item["force"]) for item in pre_rolled["plando_items"]]
if "items" not in plando_options and pre_rolled["plando_items"]:
raise Exception("Item Plando is turned off. Reusing this pre-rolled setting not permitted.")
if "plando_connections" in pre_rolled:
pre_rolled["plando_connections"] = [PlandoConnection(connection["entrance"],
connection["exit"],
connection["direction"]) for connection in pre_rolled["plando_connections"]]
if "connections" not in plando_options and pre_rolled["plando_connections"]:
raise Exception("Connection Plando is turned off. Reusing this pre-rolled setting not permitted.")
if "bosses" not in plando_options:
try:
pre_rolled["shufflebosses"] = get_plando_bosses(pre_rolled["shufflebosses"], plando_options)
except Exception as ex:
raise Exception("Boss Plando is turned off. Reusing this pre-rolled setting not permitted.") from ex
if pre_rolled.get("plando_texts") and "texts" not in plando_options:
raise Exception("Text Plando is turned off. Reusing this pre-rolled setting not permitted.")
return argparse.Namespace(**pre_rolled)
if "linked_options" in weights:
@ -514,23 +567,7 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
ret.item_functionality = get_choice('item_functionality', weights)
boss_shuffle = get_choice('boss_shuffle', weights)
if boss_shuffle in boss_shuffle_options:
ret.shufflebosses = boss_shuffle_options[boss_shuffle]
elif "bosses" in plando_options:
options = boss_shuffle.lower().split(";")
remainder_shuffle = "none" # vanilla
bosses = []
for boss in options:
if boss in boss_shuffle_options:
remainder_shuffle = boss_shuffle_options[boss]
elif boss not in available_boss_names and not "-" in boss:
raise ValueError(f"Unknown Boss name or Boss shuffle option {boss}.")
else:
bosses.append(boss)
ret.shufflebosses = ";".join(bosses + [remainder_shuffle])
else:
raise Exception(f"Boss Shuffle {boss_shuffle} is unknown and boss plando is turned off.")
ret.shufflebosses = get_plando_bosses(boss_shuffle, plando_options)
ret.enemy_shuffle = {'none': False,
'shuffled': 'shuffled',
@ -601,7 +638,7 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
ret.required_medallions[index] = {"ether": "Ether", "quake": "Quake", "bombos": "Bombos", "random": "random"}\
.get(medallion.lower(), None)
if not ret.required_medallions[index]:
raise Exception(f"unknown Medallion {medallion}")
raise Exception(f"unknown Medallion {medallion} for {'misery mire' if index == 0 else 'turtle rock'}")
inventoryweights = weights.get('startinventory', {})
startitems = []

View File

@ -52,7 +52,10 @@ def snes_to_pc(value):
def parse_player_names(names, players, teams):
names = tuple(n for n in (n.strip() for n in names.split(",")) if n)
if len(names) != len(set(names)):
raise ValueError("Duplicate Player names is not supported.")
import collections
name_counter = collections.Counter(names)
raise ValueError(f"Duplicate Player names is not supported, "
f'found multiple "{name_counter.most_common(1)[0][0]}".')
ret = []
while names or len(ret) < teams:
team = [n[:16] for n in names[:players]]

View File

@ -9,7 +9,7 @@
border-top-left-radius: 4px;
border-top-right-radius: 4px;
padding: 3px 3px 10px;
width: 260px;
width: 284px;
background-color: #42b149;
}
@ -37,7 +37,7 @@
}
#location-table{
width: 260px;
width: 284px;
border-left: 2px solid #000000;
border-right: 2px solid #000000;
border-bottom: 2px solid #000000;
@ -61,7 +61,7 @@
}
#location-table td.counter{
padding-right: 10px;
padding-right: 8px;
text-align: right;
}

View File

@ -13,6 +13,12 @@ html{
width: calc(100% - 1rem);
}
#tracker-wrapper a{
color: #234ae4;
text-decoration: none;
cursor: pointer;
}
.table-wrapper{
overflow-y: auto;
overflow-x: auto;

View File

@ -44,7 +44,8 @@
<tbody>
{%- for player, items in players.items() -%}
<tr>
<td><a href="{{ room.tracker|suuid }}/{{ team + 1 }}/{{ player }}">{{ loop.index }}</a></td>
<td><a href="{{ url_for("getPlayerTracker", tracker=room.tracker,
tracked_team=team, tracked_player=player)}}">{{ loop.index }}</a></td>
{%- if (team, loop.index) in video -%}
{%- if video[(team, loop.index)][0] == "Twitch" -%}
<td>
@ -120,7 +121,8 @@
<tbody>
{%- for player, checks in players.items() -%}
<tr>
<td><a href="{{ room.tracker|suuid }}/{{ team + 1 }}/{{ player }}">{{ loop.index }}</a></td>
<td><a href="{{ url_for("getPlayerTracker", tracker=room.tracker,
tracked_team=team, tracked_player=player)}}">{{ loop.index }}</a></td>
<td>{{ player_names[(team, loop.index)]|e }}</td>
{%- for area in ordered_areas -%}
{%- set checks_done = checks[area] -%}

View File

@ -164,7 +164,7 @@ tracking_names = ["Progressive Sword", "Progressive Bow", "Book of Mudora", "Ham
"Red Boomerang", "Bug Catching Net", "Cape", "Shovel", "Lamp",
"Mushroom", "Magic Powder",
"Cane of Somaria", "Cane of Byrna", "Fire Rod", "Ice Rod", "Bombos", "Ether", "Quake",
"Bottle", "Triforce"] # TODO make sure this list has what we need and sort it better
"Bottle", "Triforce"]
default_locations = {
'Light World': {1572864, 1572865, 60034, 1572867, 1572868, 60037, 1572869, 1572866, 60040, 59788, 60046, 60175,
@ -240,14 +240,18 @@ for item in tracking_names:
small_key_ids = {}
big_key_ids = {}
ids_small_key = {}
ids_big_key = {}
for item_name, data in Items.item_table.items():
if "Key" in item_name:
area = item_name.split("(")[1][:-1]
if "Small" in item_name:
small_key_ids[area] = data[2]
ids_small_key[data[2]] = area
else:
big_key_ids[area] = data[2]
ids_big_key[data[2]] = area
from MultiServer import get_item_name_from_id
@ -318,16 +322,25 @@ def get_static_room_data(room: Room):
for playernumber in range(1, len(names[0]) + 1)}
player_location_to_area = {playernumber: get_location_table(multidata["checks_in_area"][f'{playernumber}'])
for playernumber in range(1, len(names[0]) + 1)}
result = locations, names, use_door_tracker, player_checks_in_area, player_location_to_area
player_big_key_locations = {playernumber: set() for playernumber in range(1, len(names[0]) + 1)}
player_small_key_locations = {playernumber: set() for playernumber in range(1, len(names[0]) + 1)}
for _, (item_id, item_player) in multidata["locations"]:
if item_id in ids_big_key:
player_big_key_locations[item_player].add(ids_big_key[item_id])
if item_id in ids_small_key:
player_small_key_locations[item_player].add(ids_small_key[item_id])
result = locations, names, use_door_tracker, player_checks_in_area, player_location_to_area, player_big_key_locations, player_small_key_locations
_multidata_cache[room.seed.id] = result
return result
@app.route('/tracker/<suuid:tracker>/<int:team>/<int:player>')
@app.route('/tracker/<suuid:tracker>/<int:tracked_team>/<int:tracked_player>')
@cache.memoize(timeout=15)
def getPlayerTracker(tracker: UUID, team: int, player: int):
def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int):
# Team and player must be positive and greater than zero
if team < 1 or player < 1:
if tracked_team < 0 or tracked_player < 1:
abort(404)
room = Room.get(tracker=tracker)
@ -335,15 +348,15 @@ def getPlayerTracker(tracker: UUID, team: int, player: int):
abort(404)
# Collect seed information and pare it down to a single player
locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area = get_static_room_data(room)
player_name = names[team - 1][player - 1]
seed_checks_in_area = seed_checks_in_area[player]
player_location_to_area = player_location_to_area[player]
locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, player_big_key_locations, player_small_key_locations = get_static_room_data(room)
player_name = names[tracked_team][tracked_player - 1]
seed_checks_in_area = seed_checks_in_area[tracked_player]
location_to_area = player_location_to_area[tracked_player]
inventory = collections.Counter()
checks_done = {loc_name: 0 for loc_name in default_locations}
# Add starting items to inventory
starting_items = room.seed.multidata.get("precollected_items", None)[player - 1]
starting_items = room.seed.multidata.get("precollected_items", None)[tracked_player - 1]
if starting_items:
for item_id in starting_items:
attribute_item_solo(inventory, item_id)
@ -352,26 +365,24 @@ def getPlayerTracker(tracker: UUID, team: int, player: int):
for (ms_team, ms_player), locations_checked in room.multisave.get("location_checks", {}):
# logging.info(f"{ms_team}, {ms_player}, {locations_checked}")
# Skip teams and players not matching the request
if ms_team != (team - 1):
continue
# If the player does not have the item, do nothing
for location in locations_checked:
if (location, ms_player) not in locations or location not in player_location_to_area:
continue
if ms_team == tracked_team:
# If the player does not have the item, do nothing
for location in locations_checked:
if (location, ms_player) not in locations:
continue
item, recipient = locations[location, ms_player]
if recipient == player:
attribute_item_solo(inventory, item)
if ms_player != player:
continue
checks_done[player_location_to_area[location]] += 1
checks_done["Total"] += 1
item, recipient = locations[location, ms_player]
if recipient == tracked_player: # a check done for the tracked player
attribute_item_solo(inventory, item)
if ms_player == tracked_player: # a check done by the tracked player
checks_done[location_to_area[location]] += 1
checks_done["Total"] += 1
# Note the presence of the triforce item
for (ms_team, ms_player), game_state in room.multisave.get("client_game_state", []):
# Skip teams and players not matching the request
if ms_team != (team - 1) or ms_player != player:
if ms_team != tracked_team or ms_player != tracked_player:
continue
if game_state:
@ -462,8 +473,9 @@ def getPlayerTracker(tracker: UUID, team: int, player: int):
sword_url=sword_url, sword_acquired=sword_acquired, gloves_url=gloves_url,
gloves_acquired=gloves_acquired, bow_url=bow_url, bow_acquired=bow_acquired,
small_key_ids=small_key_ids, big_key_ids=big_key_ids, sp_areas=sp_areas,
key_locations=key_locations, big_key_locations=big_key_locations, mail_url=mail_url,
shield_url=shield_url, shield_acquired=shield_acquired)
key_locations=player_small_key_locations[tracked_player],
big_key_locations=player_big_key_locations[tracked_player],
mail_url=mail_url, shield_url=shield_url, shield_acquired=shield_acquired)
@app.route('/tracker/<suuid:tracker>')
@ -472,7 +484,7 @@ def getTracker(tracker: UUID):
room = Room.get(tracker=tracker)
if not room:
abort(404)
locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area = get_static_room_data(room)
locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, player_big_key_locations, player_small_key_locations = get_static_room_data(room)
inventory = {teamnumber: {playernumber: collections.Counter() for playernumber in range(1, len(team) + 1)}
for teamnumber, team in enumerate(names)}

View File

@ -90,6 +90,7 @@ entrance_shuffle:
# you can also define entrance shuffle seed, like so:
crossed-1000: 0 # using this method, you can have the same layout as another player and share entrance information
# however, many other settings like logic, world state, retro etc. may affect the shuffle result as well.
crossed-group-myfriends: 0 # using this method, everyone with "group-myfriends" will share the same seed
goals:
ganon: 50 # Climb GT, defeat Agahnim 2, and then kill Ganon
fast_ganon: 0 # Only killing Ganon is required. However, items may still be placed in GT

View File

@ -147,6 +147,22 @@ boss_table = {
'Agahnim2': ('Agahnim2', AgahnimDefeatRule)
}
boss_location_table = [
['Ganons Tower', 'top'],
['Tower of Hera', None],
['Skull Woods', None],
['Ganons Tower', 'middle'],
['Eastern Palace', None],
['Desert Palace', None],
['Palace of Darkness', None],
['Swamp Palace', None],
['Thieves Town', None],
['Ice Palace', None],
['Misery Mire', None],
['Turtle Rock', None],
['Ganons Tower', 'bottom'],
]
def can_place_boss(boss: str, dungeon_name: str, level: Optional[str] = None) -> bool:
# blacklist approach
@ -183,21 +199,7 @@ def place_bosses(world, player: int):
if world.boss_shuffle[player] == 'none':
return
# Most to least restrictive order
boss_locations = [
['Ganons Tower', 'top'],
['Tower of Hera', None],
['Skull Woods', None],
['Ganons Tower', 'middle'],
['Eastern Palace', None],
['Desert Palace', None],
['Palace of Darkness', None],
['Swamp Palace', None],
['Thieves Town', None],
['Ice Palace', None],
['Misery Mire', None],
['Turtle Rock', None],
['Ganons Tower', 'bottom'],
]
boss_locations = boss_location_table.copy()
all_bosses = sorted(boss_table.keys()) # sorted to be deterministic on older pythons
placeable_bosses = [boss for boss in all_bosses if boss not in ['Agahnim', 'Agahnim2', 'Ganon']]
@ -217,13 +219,13 @@ def place_bosses(world, player: int):
loc = loc.split(" ")
level = loc[-1]
loc = " ".join(loc[:-1])
loc = loc.title()
loc = loc.title().replace("Of", "of")
if can_place_boss(boss, loc, level) and [loc, level] in boss_locations:
place_boss(world, player, boss, loc, level)
already_placed_bosses.append(boss)
boss_locations.remove([loc, level])
else:
Exception("Cannot place", boss, "at", loc, level, "for player", player)
raise Exception("Cannot place", boss, "at", loc, level, "for player", player)
else:
boss = boss.title()
boss_locations, already_placed_bosses = place_where_possible(world, player, boss, boss_locations)