Make hint costs relative

This commit is contained in:
Fabian Dill 2021-05-11 23:08:50 +02:00
parent 1b2283b173
commit a231850911
7 changed files with 62 additions and 59 deletions

View File

@ -129,7 +129,7 @@ class CommonContext():
self.input_requests = 0 self.input_requests = 0
# game state # game state
self.player_names: typing.Dict[int: str] = {0: "Server"} self.player_names: typing.Dict[int: str] = {0: "Archipelago"}
self.exit_event = asyncio.Event() self.exit_event = asyncio.Event()
self.watcher_event = asyncio.Event() self.watcher_event = asyncio.Event()
@ -194,7 +194,7 @@ class CommonContext():
def consume_players_package(self, package: typing.List[tuple]): def consume_players_package(self, package: typing.List[tuple]):
self.player_names = {slot: name for team, slot, name, orig_name in package if self.team == team} self.player_names = {slot: name for team, slot, name, orig_name in package if self.team == team}
self.player_names[0] = "Server" self.player_names[0] = "Archipelago"
def event_invalid_slot(self): def event_invalid_slot(self):
raise Exception('Invalid Slot; please verify that you have connected to the correct world.') raise Exception('Invalid Slot; please verify that you have connected to the correct world.')
@ -305,8 +305,8 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
logger.info('Password required') logger.info('Password required')
logger.info(f"Forfeit setting: {args['forfeit_mode']}") logger.info(f"Forfeit setting: {args['forfeit_mode']}")
logger.info(f"Remaining setting: {args['remaining_mode']}") logger.info(f"Remaining setting: {args['remaining_mode']}")
logger.info(f"A !hint costs {args['hint_cost']} points and you get {args['location_check_points']}" logger.info(f"A !hint costs {args['hint_cost']}% of checks points and you get {args['location_check_points']}"
f" for each location checked.") f" for each location checked. Use !hint for more information.")
ctx.hint_cost = int(args['hint_cost']) ctx.hint_cost = int(args['hint_cost'])
ctx.check_points = int(args['location_check_points']) ctx.check_points = int(args['location_check_points'])
ctx.forfeit_mode = args['forfeit_mode'] ctx.forfeit_mode = args['forfeit_mode']

15
Main.py
View File

@ -7,7 +7,7 @@ import time
import zlib import zlib
import concurrent.futures import concurrent.futures
import pickle import pickle
from typing import Dict from typing import Dict, Tuple
from BaseClasses import MultiWorld, CollectionState, Region, Item from BaseClasses import MultiWorld, CollectionState, Region, Item
from worlds.alttp.Items import ItemFactory, item_name_groups from worlds.alttp.Items import ItemFactory, item_name_groups
@ -501,7 +501,7 @@ def main(args, seed=None):
rom_names.append(rom_name) rom_names.append(rom_name)
slot_data = {} slot_data = {}
client_versions = {} client_versions = {}
minimum_versions = {"server": (0, 0, 4), "clients": client_versions} minimum_versions = {"server": (0, 1, 1), "clients": client_versions}
games = {} games = {}
for slot in world.player_ids: for slot in world.player_ids:
client_versions[slot] = (0, 0, 3) client_versions[slot] = (0, 0, 3)
@ -520,6 +520,11 @@ def main(args, seed=None):
slots_data[option_name] = int(option.value) slots_data[option_name] = int(option.value)
for slot in world.minecraft_player_ids: for slot in world.minecraft_player_ids:
slot_data[slot] = fill_minecraft_slot_data(world, slot) slot_data[slot] = fill_minecraft_slot_data(world, slot)
locations_data: Dict[int, Dict[int, Tuple[int, int]]] = {player: {} for player in world.player_ids}
for location in world.get_filled_locations():
if type(location.address) == int:
locations_data[location.player][location.address] = (location.item.code, location.item.player)
multidata = zlib.compress(pickle.dumps({ multidata = zlib.compress(pickle.dumps({
"slot_data" : slot_data, "slot_data" : slot_data,
"games": games, "games": games,
@ -527,11 +532,7 @@ def main(args, seed=None):
"connect_names": connect_names, "connect_names": connect_names,
"remote_items": {player for player in range(1, world.players + 1) if "remote_items": {player for player in range(1, world.players + 1) if
world.remote_items[player]}, world.remote_items[player]},
"locations": { "locations": locations_data,
(location.address, location.player):
(location.item.code, location.item.player)
for location in world.get_filled_locations() if
type(location.address) is int},
"checks_in_area": checks_in_area, "checks_in_area": checks_in_area,
"server_options": get_options()["server_options"], "server_options": get_options()["server_options"],
"er_hint_data": er_hint_data, "er_hint_data": er_hint_data,

View File

@ -78,7 +78,7 @@ class Context(Node):
self.connect_names = {} # names of slots clients can connect to self.connect_names = {} # names of slots clients can connect to
self.allow_forfeits = {} self.allow_forfeits = {}
self.remote_items = set() self.remote_items = set()
self.locations = {} self.locations:typing.Dict[int, typing.Dict[int, typing.Tuple[int, int]]] = {}
self.host = host self.host = host
self.port = port self.port = port
self.server_password = server_password self.server_password = server_password
@ -114,6 +114,11 @@ class Context(Node):
self.minimum_client_versions: typing.Dict[int, Utils.Version] = {} self.minimum_client_versions: typing.Dict[int, Utils.Version] = {}
self.seed_name = "" self.seed_name = ""
def get_hint_cost(self, slot):
if self.hint_cost:
return max(0, int(self.hint_cost * 0.01 * len(self.locations[slot])))
return 0
def load(self, multidatapath: str, use_embedded_server_options: bool = False): def load(self, multidatapath: str, use_embedded_server_options: bool = False):
with open(multidatapath, 'rb') as f: with open(multidatapath, 'rb') as f:
data = f.read() data = f.read()
@ -132,7 +137,7 @@ class Context(Node):
mdata_ver = decoded_obj["minimum_versions"]["server"] mdata_ver = decoded_obj["minimum_versions"]["server"]
if mdata_ver > Utils._version_tuple: if mdata_ver > Utils._version_tuple:
raise RuntimeError(f"Supplied Multidata requires a server of at least version {mdata_ver}," raise RuntimeError(f"Supplied Multidata (.archipelago) requires a server of at least version {mdata_ver},"
f"however this server is of version {Utils._version_tuple}") f"however this server is of version {Utils._version_tuple}")
clients_ver = decoded_obj["minimum_versions"].get("clients", {}) clients_ver = decoded_obj["minimum_versions"].get("clients", {})
self.minimum_client_versions = {} self.minimum_client_versions = {}
@ -437,10 +442,6 @@ def get_received_items(ctx: Context, team: int, player: int) -> typing.List[Netw
return ctx.received_items.setdefault((team, player), []) return ctx.received_items.setdefault((team, player), [])
def tuplize_received_items(items):
return [NetworkItem(item.item, item.location, item.player) for item in items]
def send_new_items(ctx: Context): def send_new_items(ctx: Context):
for client in ctx.endpoints: for client in ctx.endpoints:
if client.auth: # can't send to disconnected client if client.auth: # can't send to disconnected client
@ -449,22 +450,22 @@ def send_new_items(ctx: Context):
asyncio.create_task(ctx.send_msgs(client, [{ asyncio.create_task(ctx.send_msgs(client, [{
"cmd": "ReceivedItems", "cmd": "ReceivedItems",
"index": client.send_index, "index": client.send_index,
"items": tuplize_received_items(items)[client.send_index:]}])) "items": items[client.send_index:]}]))
client.send_index = len(items) client.send_index = len(items)
def forfeit_player(ctx: Context, team: int, slot: int): def forfeit_player(ctx: Context, team: int, slot: int):
# register any locations that are in the multidata # register any locations that are in the multidata
all_locations = {location_id for location_id, location_slot in ctx.locations if location_slot == slot} all_locations = set(ctx.locations[slot])
ctx.notify_all("%s (Team #%d) has forfeited" % (ctx.player_names[(team, slot)], team + 1)) ctx.notify_all("%s (Team #%d) has forfeited" % (ctx.player_names[(team, slot)], team + 1))
register_location_checks(ctx, team, slot, all_locations) register_location_checks(ctx, team, slot, all_locations)
def get_remaining(ctx: Context, team: int, slot: int) -> typing.List[int]: def get_remaining(ctx: Context, team: int, slot: int) -> typing.List[int]:
items = [] items = []
for (location, location_slot) in ctx.locations: for location_id in ctx.locations[slot]:
if location_slot == slot and location not in ctx.location_checks[team, slot]: if location_id not in ctx.location_checks[team, slot]:
items.append(ctx.locations[location, slot][0]) # item ID items.append(ctx.locations[slot][location_id][0]) # item ID
return sorted(items) return sorted(items)
@ -473,8 +474,8 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi
if new_locations: if new_locations:
ctx.client_activity_timers[team, slot] = datetime.datetime.now(datetime.timezone.utc) ctx.client_activity_timers[team, slot] = datetime.datetime.now(datetime.timezone.utc)
for location in new_locations: for location in new_locations:
if (location, slot) in ctx.locations: if location in ctx.locations[slot]:
item_id, target_player = ctx.locations[(location, slot)] item_id, target_player = ctx.locations[slot][location]
new_item = NetworkItem(item_id, location, slot) new_item = NetworkItem(item_id, location, slot)
if target_player != slot or slot in ctx.remote_items: if target_player != slot or slot in ctx.remote_items:
get_received_items(ctx, team, target_player).append(new_item) get_received_items(ctx, team, target_player).append(new_item)
@ -504,29 +505,26 @@ def notify_team(ctx: Context, team: int, text: str):
def collect_hints(ctx: Context, team: int, slot: int, item: str) -> typing.List[NetUtils.Hint]: def collect_hints(ctx: Context, team: int, slot: int, item: str) -> typing.List[NetUtils.Hint]:
hints = [] hints = []
seeked_item_id = lookup_any_item_name_to_id[item] seeked_item_id = lookup_any_item_name_to_id[item]
for check, result in ctx.locations.items(): for finding_player, check_data in ctx.locations.items():
item_id, receiving_player = result for location_id, result in check_data.items():
if receiving_player == slot and item_id == seeked_item_id: item_id, receiving_player = result
location_id, finding_player = check if receiving_player == slot and item_id == seeked_item_id:
found = location_id in ctx.location_checks[team, finding_player] found = location_id in ctx.location_checks[team, finding_player]
entrance = ctx.er_hint_data.get(finding_player, {}).get(location_id, "") entrance = ctx.er_hint_data.get(finding_player, {}).get(location_id, "")
hints.append(NetUtils.Hint(receiving_player, finding_player, location_id, item_id, found, entrance)) hints.append(NetUtils.Hint(receiving_player, finding_player, location_id, item_id, found, entrance))
return hints return hints
def collect_hints_location(ctx: Context, team: int, slot: int, location: str) -> typing.List[NetUtils.Hint]: def collect_hints_location(ctx: Context, team: int, slot: int, location: str) -> typing.List[NetUtils.Hint]:
hints = []
seeked_location = Regions.lookup_name_to_id[location] seeked_location: int = Regions.lookup_name_to_id[location]
for check, result in ctx.locations.items(): item_id, receiving_player = ctx.locations[slot].get(seeked_location, (None, None))
location_id, finding_player = check if item_id:
if finding_player == slot and location_id == seeked_location: found = seeked_location in ctx.location_checks[team, slot]
item_id, receiving_player = result entrance = ctx.er_hint_data.get(slot, {}).get(seeked_location, "")
found = location_id in ctx.location_checks[team, finding_player] return [NetUtils.Hint(receiving_player, slot, seeked_location, item_id, found, entrance)]
entrance = ctx.er_hint_data.get(finding_player, {}).get(location_id, "") return []
hints.append(NetUtils.Hint(receiving_player, finding_player, location_id, item_id, found, entrance))
break # each location has 1 item
return hints
def format_hint(ctx: Context, team: int, hint: NetUtils.Hint) -> str: def format_hint(ctx: Context, team: int, hint: NetUtils.Hint) -> str:
@ -864,7 +862,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
"""Use !hint {item_name/location_name}, for example !hint Lamp or !hint Link's House. """ """Use !hint {item_name/location_name}, for example !hint Lamp or !hint Link's House. """
points_available = get_client_points(self.ctx, self.client) points_available = get_client_points(self.ctx, self.client)
if not item_or_location: if not item_or_location:
self.output(f"A hint costs {self.ctx.hint_cost} points. " self.output(f"A hint costs {self.ctx.get_hint_cost(self.client.slot)} points. "
f"You have {points_available} points.") f"You have {points_available} points.")
hints = {hint.re_check(self.ctx, self.client.team) for hint in hints = {hint.re_check(self.ctx, self.client.team) for hint in
self.ctx.hints[self.client.team, self.client.slot]} self.ctx.hints[self.client.team, self.client.slot]}
@ -885,7 +883,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
hints = collect_hints(self.ctx, self.client.team, self.client.slot, item_name) hints = collect_hints(self.ctx, self.client.team, self.client.slot, item_name)
else: # location name else: # location name
hints = collect_hints_location(self.ctx, self.client.team, self.client.slot, item_name) hints = collect_hints_location(self.ctx, self.client.team, self.client.slot, item_name)
cost = self.ctx.get_hint_cost(self.client.slot)
if hints: if hints:
new_hints = set(hints) - self.ctx.hints[self.client.team, self.client.slot] new_hints = set(hints) - self.ctx.hints[self.client.team, self.client.slot]
old_hints = set(hints) - new_hints old_hints = set(hints) - new_hints
@ -899,8 +897,8 @@ class ClientMessageProcessor(CommonCommandProcessor):
if not not_found_hints: # everything's been found, no need to pay if not not_found_hints: # everything's been found, no need to pay
can_pay = 1000 can_pay = 1000
elif self.ctx.hint_cost: elif cost:
can_pay = points_available // self.ctx.hint_cost can_pay = points_available // cost
else: else:
can_pay = 1000 can_pay = 1000
@ -926,7 +924,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
else: else:
self.output(f"You can't afford the hint. " self.output(f"You can't afford the hint. "
f"You have {points_available} points and need at least " f"You have {points_available} points and need at least "
f"{self.ctx.hint_cost}") f"{self.ctx.get_hint_cost(self.client.slot)}")
notify_hints(self.ctx, self.client.team, hints) notify_hints(self.ctx, self.client.team, hints)
self.ctx.save() self.ctx.save()
return True return True
@ -941,21 +939,19 @@ class ClientMessageProcessor(CommonCommandProcessor):
def get_checked_checks(ctx: Context, client: Client) -> typing.List[int]: def get_checked_checks(ctx: Context, client: Client) -> typing.List[int]:
return [location_id for return [location_id for
location_id, slot in ctx.locations if location_id in ctx.locations[client.slot] if
slot == client.slot and
location_id in ctx.location_checks[client.team, client.slot]] location_id in ctx.location_checks[client.team, client.slot]]
def get_missing_checks(ctx: Context, client: Client) -> typing.List[int]: def get_missing_checks(ctx: Context, client: Client) -> typing.List[int]:
return [location_id for return [location_id for
location_id, slot in ctx.locations if location_id in ctx.locations[client.slot] if
slot == client.slot and
location_id not in ctx.location_checks[client.team, client.slot]] location_id not in ctx.location_checks[client.team, client.slot]]
def get_client_points(ctx: Context, client: Client) -> int: def get_client_points(ctx: Context, client: Client) -> int:
return (ctx.location_check_points * len(ctx.location_checks[client.team, client.slot]) - return (ctx.location_check_points * len(ctx.location_checks[client.team, client.slot]) -
ctx.hint_cost * ctx.hints_used[client.team, client.slot]) ctx.get_hint_cost(client.slot) * ctx.hints_used[client.team, client.slot])
async def process_client_cmd(ctx: Context, client: Client, args: dict): async def process_client_cmd(ctx: Context, client: Client, args: dict):
@ -1032,7 +1028,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
}] }]
items = get_received_items(ctx, client.team, client.slot) items = get_received_items(ctx, client.team, client.slot)
if items: if items:
reply.append({"cmd": 'ReceivedItems', "index": 0, "items": tuplize_received_items(items)}) reply.append({"cmd": 'ReceivedItems', "index": 0, "items": items})
client.send_index = len(items) client.send_index = len(items)
await ctx.send_msgs(client, reply) await ctx.send_msgs(client, reply)
@ -1047,7 +1043,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
if items: if items:
client.send_index = len(items) client.send_index = len(items)
await ctx.send_msgs(client, [{"cmd": "ReceivedItems","index": 0, await ctx.send_msgs(client, [{"cmd": "ReceivedItems","index": 0,
"items": tuplize_received_items(items)}]) "items": items}])
elif cmd == 'LocationChecks': elif cmd == 'LocationChecks':
register_location_checks(ctx, client.team, client.slot, args["locations"]) register_location_checks(ctx, client.team, client.slot, args["locations"])
@ -1058,7 +1054,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
if type(location) is not int or location not in lookup_any_location_id_to_name: if type(location) is not int or location not in lookup_any_location_id_to_name:
await ctx.send_msgs(client, [{"cmd": "InvalidArguments", "text": 'LocationScouts'}]) await ctx.send_msgs(client, [{"cmd": "InvalidArguments", "text": 'LocationScouts'}])
return return
target_item, target_player = ctx.locations[location, client.slot] target_item, target_player = ctx.locations[client.slot][location]
locs.append(NetworkItem(target_item, location, target_player)) locs.append(NetworkItem(target_item, location, target_player))
await ctx.send_msgs(client, [{'cmd': 'LocationInfo', 'locations': locs}]) await ctx.send_msgs(client, [{'cmd': 'LocationInfo', 'locations': locs}])
@ -1206,7 +1202,7 @@ class ServerCommandProcessor(CommonCommandProcessor):
if usable: if usable:
for client in self.ctx.endpoints: for client in self.ctx.endpoints:
if client.name == seeked_player: if client.name == seeked_player:
new_item = NetworkItem(lookup_any_item_name_to_id[item], -1, client.slot) new_item = NetworkItem(lookup_any_item_name_to_id[item], -1, 0)
get_received_items(self.ctx, client.team, client.slot).append(new_item) get_received_items(self.ctx, client.team, client.slot).append(new_item)
self.ctx.notify_all('Cheat console: sending "' + item + '" to ' + self.ctx.notify_all('Cheat console: sending "' + item + '" to ' +
self.ctx.get_aliased_name(client.team, client.slot)) self.ctx.get_aliased_name(client.team, client.slot))

View File

@ -303,7 +303,10 @@ def handle_name(name: str, player: int, name_counter: Counter):
name] > 1 else ''), name] > 1 else ''),
player=player, player=player,
PLAYER=(player if player > 1 else ''))) PLAYER=(player if player > 1 else '')))
return new_name.strip().replace(' ', '_')[:16] new_name = new_name.strip().replace(' ', '_')[:16]
if new_name == "Archipelago":
raise Exception(f"You cannot name yourself \"{new_name}\"")
return new_name
def prefer_int(input_data: str) -> typing.Union[str, int]: def prefer_int(input_data: str) -> typing.Union[str, int]:

View File

@ -12,7 +12,7 @@ class Version(typing.NamedTuple):
minor: int minor: int
build: int build: int
__version__ = "0.1.0" __version__ = "0.1.1"
_version_tuple = tuplize_version(__version__) _version_tuple = tuplize_version(__version__)
import builtins import builtins

View File

@ -19,7 +19,8 @@ server_options:
# Client hint system # Client hint system
# Points given to a player for each acquired item in their world # Points given to a player for each acquired item in their world
location_check_points: 1 location_check_points: 1
# Point cost to receive a hint via !hint for players # Relative point cost to receive a hint via !hint for players
# so for example hint_cost: 20 would mean that for every 20% of available checks, you get the ability to hint, for a total of 5
hint_cost: 1000 # Set to 0 if you want free hints hint_cost: 1000 # Set to 0 if you want free hints
# Forfeit modes # Forfeit modes
# "disabled" -> clients can't forfeit, # "disabled" -> clients can't forfeit,

View File

@ -13,6 +13,7 @@ from .minecraft.Items import lookup_id_to_name as mc
lookup_any_item_id_to_name = {**alttp, **hk, **Technologies.lookup_id_to_name, **mc} lookup_any_item_id_to_name = {**alttp, **hk, **Technologies.lookup_id_to_name, **mc}
assert len(alttp) + len(hk) + len(Technologies.lookup_id_to_name) + len(mc) == len(lookup_any_item_id_to_name) assert len(alttp) + len(hk) + len(Technologies.lookup_id_to_name) + len(mc) == len(lookup_any_item_id_to_name)
lookup_any_item_name_to_id = {name: id for id, name in lookup_any_item_id_to_name.items()} lookup_any_item_name_to_id = {name: id for id, name in lookup_any_item_id_to_name.items()}
# assert len(lookup_any_item_name_to_id) == len(lookup_any_item_id_to_name) # currently broken: Single Arrow
from .alttp import Regions from .alttp import Regions
from .hk import Locations from .hk import Locations
@ -24,6 +25,7 @@ assert len(Regions.lookup_id_to_name) + len(Locations.lookup_id_to_name) + \
len(Technologies.lookup_id_to_name) + len(Advancements.lookup_id_to_name) == \ len(Technologies.lookup_id_to_name) + len(Advancements.lookup_id_to_name) == \
len(lookup_any_location_id_to_name) len(lookup_any_location_id_to_name)
lookup_any_location_name_to_id = {name: id for id, name in lookup_any_location_id_to_name.items()} lookup_any_location_name_to_id = {name: id for id, name in lookup_any_location_id_to_name.items()}
assert len(lookup_any_location_name_to_id) == len(lookup_any_location_id_to_name)
network_data_package = {"lookup_any_location_id_to_name": lookup_any_location_id_to_name, network_data_package = {"lookup_any_location_id_to_name": lookup_any_location_id_to_name,
"lookup_any_item_id_to_name": lookup_any_item_id_to_name, "lookup_any_item_id_to_name": lookup_any_item_id_to_name,