Added my game made specifically for AP, ChecksFinder (Minesweeper) (#302)

This commit is contained in:
jonloveslegos 2022-03-22 18:30:10 -04:00 committed by GitHub
parent 7f1371ec00
commit 4bf8b98681
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 993 additions and 0 deletions

658
ChecksFinderClient.py Normal file
View File

@ -0,0 +1,658 @@
from __future__ import annotations
import os
import logging
import asyncio
import urllib.parse
import sys
import typing
import time
import websockets
import Utils
if __name__ == "__main__":
Utils.init_logging("ChecksFinderClient", exception_logger="Client")
from MultiServer import CommandProcessor
from NetUtils import Endpoint, decode, NetworkItem, encode, JSONtoTextParser, ClientStatus, Permission
from Utils import Version, stream_input
from worlds import network_data_package, AutoWorldRegister
from CommonClient import gui_enabled, console_loop, logger, server_autoreconnect, get_base_parser, \
keep_alive
from worlds.checksfinder import ChecksFinderWorld
class ClientCommandProcessor(CommandProcessor):
def __init__(self, ctx: CommonContext):
self.ctx = ctx
def output(self, text: str):
logger.info(text)
def _cmd_exit(self) -> bool:
"""Close connections and client"""
self.ctx.exit_event.set()
return True
def _cmd_connect(self, address: str = "") -> bool:
"""Connect to a MultiWorld Server"""
self.ctx.server_address = None
asyncio.create_task(self.ctx.connect(address if address else None), name="connecting")
return True
def _cmd_disconnect(self) -> bool:
"""Disconnect from a MultiWorld Server"""
self.ctx.server_address = None
asyncio.create_task(self.ctx.disconnect(), name="disconnecting")
return True
def _cmd_received(self) -> bool:
"""List all received items"""
logger.info(f'{len(self.ctx.items_received)} received items:')
for index, item in enumerate(self.ctx.items_received, 1):
self.output(f"{self.ctx.item_name_getter(item.item)} from {self.ctx.player_names[item.player]}")
return True
def _cmd_missing(self) -> bool:
"""List all missing location checks, from your local game state"""
if not self.ctx.game:
self.output("No game set, cannot determine missing checks.")
return False
count = 0
checked_count = 0
for location, location_id in AutoWorldRegister.world_types[self.ctx.game].location_name_to_id.items():
if location_id < 0:
continue
if location_id not in self.ctx.locations_checked:
if location_id in self.ctx.missing_locations:
self.output('Missing: ' + location)
count += 1
elif location_id in self.ctx.checked_locations:
self.output('Checked: ' + location)
count += 1
checked_count += 1
if count:
self.output(
f"Found {count} missing location checks{f'. {checked_count} location checks previously visited.' if checked_count else ''}")
else:
self.output("No missing location checks found.")
return True
def _cmd_items(self):
"""List all item names for the currently running game."""
self.output(f"Item Names for {self.ctx.game}")
for item_name in AutoWorldRegister.world_types[self.ctx.game].item_name_to_id:
self.output(item_name)
def _cmd_locations(self):
"""List all location names for the currently running game."""
self.output(f"Location Names for {self.ctx.game}")
for location_name in AutoWorldRegister.world_types[self.ctx.game].location_name_to_id:
self.output(location_name)
def _cmd_resync(self):
"""Manually trigger a resync."""
self.output(f"Syncing items.")
self.ctx.syncing = True
def _cmd_ready(self):
"""Send ready status to server."""
self.ctx.ready = not self.ctx.ready
if self.ctx.ready:
state = ClientStatus.CLIENT_READY
self.output("Readied up.")
else:
state = ClientStatus.CLIENT_CONNECTED
self.output("Unreadied.")
asyncio.create_task(self.ctx.send_msgs([{"cmd": "StatusUpdate", "status": state}]), name="send StatusUpdate")
def default(self, raw: str):
raw = self.ctx.on_user_say(raw)
if raw:
asyncio.create_task(self.ctx.send_msgs([{"cmd": "Say", "text": raw}]), name="send Say")
class CommonContext():
tags: typing.Set[str] = {"AP"}
starting_reconnect_delay: int = 5
current_reconnect_delay: int = starting_reconnect_delay
command_processor: int = ClientCommandProcessor
game = None
ui = None
keep_alive_task = None
items_handling: typing.Optional[int] = None
current_energy_link_value = 0 # to display in UI, gets set by server
def __init__(self, server_address, password):
# server state
self.send_index: int = 0
self.server_address = server_address
self.password = password
self.syncing = False
self.awaiting_bridge = False
self.server_task = None
self.server: typing.Optional[Endpoint] = None
self.server_version = Version(0, 0, 0)
self.hint_cost: typing.Optional[int] = None
self.games: typing.Dict[int, str] = {}
self.permissions = {
"forfeit": "disabled",
"collect": "disabled",
"remaining": "disabled",
}
# own state
self.finished_game = False
self.ready = False
self.team = None
self.slot = None
self.auth = None
self.seed_name = None
self.locations_checked: typing.Set[int] = set() # local state
self.locations_scouted: typing.Set[int] = set()
self.items_received = []
self.missing_locations: typing.Set[int] = set()
self.checked_locations: typing.Set[int] = set() # server state
self.locations_info = {}
self.input_queue = asyncio.Queue()
self.input_requests = 0
self.last_death_link: float = time.time() # last send/received death link on AP layer
# game state
self.player_names: typing.Dict[int: str] = {0: "Archipelago"}
self.exit_event = asyncio.Event()
self.watcher_event = asyncio.Event()
self.slow_mode = False
self.jsontotextparser = JSONtoTextParser(self)
self.set_getters(network_data_package)
# execution
self.keep_alive_task = asyncio.create_task(keep_alive(self), name="Bouncy")
@property
def total_locations(self) -> typing.Optional[int]:
"""Will return None until connected."""
if self.checked_locations or self.missing_locations:
return len(self.checked_locations | self.missing_locations)
async def connection_closed(self):
self.auth = None
self.items_received = []
self.locations_info = {}
self.server_version = Version(0, 0, 0)
if self.server and self.server.socket is not None:
await self.server.socket.close()
self.server = None
self.server_task = None
path = os.path.expandvars(r"%localappdata%/ChecksFinder")
for root, dirs, files in os.walk(path):
for file in files:
if file.find("obtain") <= -1:
os.remove(root+"/"+file)
# noinspection PyAttributeOutsideInit
def set_getters(self, data_package: dict, network=False):
if not network: # local data; check if newer data was already downloaded
local_package = Utils.persistent_load().get("datapackage", {}).get("latest", {})
if local_package and local_package["version"] > network_data_package["version"]:
data_package: dict = local_package
elif network: # check if data from server is newer
if data_package["version"] > network_data_package["version"]:
Utils.persistent_store("datapackage", "latest", network_data_package)
item_lookup: dict = {}
locations_lookup: dict = {}
for game, gamedata in data_package["games"].items():
for item_name, item_id in gamedata["item_name_to_id"].items():
item_lookup[item_id] = item_name
for location_name, location_id in gamedata["location_name_to_id"].items():
locations_lookup[location_id] = location_name
def get_item_name_from_id(code: int):
return item_lookup.get(code, f'Unknown item (ID:{code})')
self.item_name_getter = get_item_name_from_id
def get_location_name_from_address(address: int):
return locations_lookup.get(address, f'Unknown location (ID:{address})')
self.location_name_getter = get_location_name_from_address
@property
def endpoints(self):
if self.server:
return [self.server]
else:
return []
async def disconnect(self):
if self.server and not self.server.socket.closed:
await self.server.socket.close()
if self.server_task is not None:
await self.server_task
async def send_msgs(self, msgs):
if not self.server or not self.server.socket.open or self.server.socket.closed:
return
await self.server.socket.send(encode(msgs))
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[0] = "Archipelago"
def event_invalid_slot(self):
raise Exception('Invalid Slot; please verify that you have connected to the correct world.')
def event_invalid_game(self):
raise Exception('Invalid Game; please verify that you connected with the right game to the correct world.')
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
logger.info('Enter the password required to join this game:')
self.password = await self.console_input()
return self.password
async def send_connect(self, **kwargs):
payload = {
'cmd': 'Connect',
'password': self.password, 'name': self.auth, 'version': Utils.version_tuple,
'tags': self.tags, 'items_handling': self.items_handling,
'uuid': Utils.get_unique_identifier(), 'game': self.game
}
if kwargs:
payload.update(kwargs)
await self.send_msgs([payload])
async def console_input(self):
self.input_requests += 1
return await self.input_queue.get()
async def connect(self, address=None):
await self.disconnect()
self.server_task = asyncio.create_task(server_loop(self, address), name="server loop")
def on_print(self, args: dict):
logger.info(args["text"])
def on_print_json(self, args: dict):
if self.ui:
self.ui.print_json(args["data"])
else:
text = self.jsontotextparser(args["data"])
logger.info(text)
def on_package(self, cmd: str, args: dict):
pass
def on_user_say(self, text: str) -> typing.Optional[str]:
"""Gets called before sending a Say to the server from the user.
Returned text is sent, or sending is aborted if None is returned."""
return text
def update_permissions(self, permissions: typing.Dict[str, int]):
for permission_name, permission_flag in permissions.items():
try:
flag = Permission(permission_flag)
logger.info(f"{permission_name.capitalize()} permission: {flag.name}")
self.permissions[permission_name] = flag.name
except Exception as e: # safeguard against permissions that may be implemented in the future
logger.exception(e)
async def shutdown(self):
self.server_address = None
if self.server and not self.server.socket.closed:
await self.server.socket.close()
if self.server_task:
await self.server_task
while self.input_requests > 0:
self.input_queue.put_nowait(None)
self.input_requests -= 1
self.keep_alive_task.cancel()
path = os.path.expandvars(r"%localappdata%/ChecksFinder")
for root, dirs, files in os.walk(path):
for file in files:
if file.find("obtain") <= -1:
os.remove(root+"/"+file)
# DeathLink hooks
def on_deathlink(self, data: dict):
"""Gets dispatched when a new DeathLink is triggered by another linked player."""
self.last_death_link = max(data["time"], self.last_death_link)
text = data.get("cause", "")
if text:
logger.info(f"DeathLink: {text}")
else:
logger.info(f"DeathLink: Received from {data['source']}")
async def send_death(self, death_text: str = ""):
if self.server and self.server.socket:
logger.info("DeathLink: Sending death to your friends...")
self.last_death_link = time.time()
await self.send_msgs([{
"cmd": "Bounce", "tags": ["DeathLink"],
"data": {
"time": self.last_death_link,
"source": self.player_names[self.slot],
"cause": death_text
}
}])
async def update_death_link(self, death_link):
old_tags = self.tags.copy()
if death_link:
self.tags.add("DeathLink")
else:
self.tags -= {"DeathLink"}
if old_tags != self.tags and self.server and not self.server.socket.closed:
await self.send_msgs([{"cmd": "ConnectUpdate", "tags": self.tags}])
async def server_loop(ctx: CommonContext, address=None):
cached_address = None
if ctx.server and ctx.server.socket:
logger.error('Already connected')
return
if address is None: # set through CLI or APBP
address = ctx.server_address
# Wait for the user to provide a multiworld server address
if not address:
logger.info('Please connect to an Archipelago server.')
return
address = f"ws://{address}" if "://" not in address else address
port = urllib.parse.urlparse(address).port or 38281
logger.info(f'Connecting to Archipelago server at {address}')
try:
socket = await websockets.connect(address, port=port, ping_timeout=None, ping_interval=None)
ctx.server = Endpoint(socket)
logger.info('Connected')
ctx.server_address = address
ctx.current_reconnect_delay = ctx.starting_reconnect_delay
async for data in ctx.server.socket:
for msg in decode(data):
await process_server_cmd(ctx, msg)
logger.warning('Disconnected from multiworld server, type /connect to reconnect')
except ConnectionRefusedError:
if cached_address:
logger.error('Unable to connect to multiworld server at cached address. '
'Please use the connect button above.')
else:
logger.exception('Connection refused by the multiworld server')
except websockets.InvalidURI:
logger.exception('Failed to connect to the multiworld server (invalid URI)')
except (OSError, websockets.InvalidURI):
logger.exception('Failed to connect to the multiworld server')
except Exception as e:
logger.exception('Lost connection to the multiworld server, type /connect to reconnect')
finally:
await ctx.connection_closed()
if ctx.server_address:
logger.info(f"... reconnecting in {ctx.current_reconnect_delay}s")
asyncio.create_task(server_autoreconnect(ctx), name="server auto reconnect")
ctx.current_reconnect_delay *= 2
async def process_server_cmd(ctx: CommonContext, args: dict):
try:
cmd = args["cmd"]
except:
logger.exception(f"Could not get command from {args}")
raise
if cmd == 'RoomInfo':
if ctx.seed_name and ctx.seed_name != args["seed_name"]:
logger.info("The server is running a different multiworld than your client is. (invalid seed_name)")
else:
logger.info('--------------------------------')
logger.info('Room Information:')
logger.info('--------------------------------')
version = args["version"]
ctx.server_version = tuple(version)
version = ".".join(str(item) for item in version)
logger.info(f'Server protocol version: {version}')
logger.info("Server protocol tags: " + ", ".join(args["tags"]))
if args['password']:
logger.info('Password required')
ctx.update_permissions(args.get("permissions", {}))
if "games" in args:
ctx.games = {x: game for x, game in enumerate(args["games"], start=1)}
logger.info(
f"A !hint costs {args['hint_cost']}% of your total location count as points"
f" and you get {args['location_check_points']}"
f" for each location checked. Use !hint for more information.")
ctx.hint_cost = int(args['hint_cost'])
ctx.check_points = int(args['location_check_points'])
if len(args['players']) < 1:
logger.info('No player connected')
else:
args['players'].sort()
current_team = -1
logger.info('Connected Players:')
for network_player in args['players']:
if network_player.team != current_team:
logger.info(f' Team #{network_player.team + 1}')
current_team = network_player.team
logger.info(' %s (Player %d)' % (network_player.alias, network_player.slot))
if args["datapackage_version"] > network_data_package["version"] or args["datapackage_version"] == 0:
await ctx.send_msgs([{"cmd": "GetDataPackage"}])
await ctx.server_auth(args['password'])
elif cmd == 'DataPackage':
logger.info("Got new ID/Name Datapackage")
ctx.set_getters(args['data'], network=True)
elif cmd == 'ConnectionRefused':
errors = args["errors"]
if 'InvalidSlot' in errors:
ctx.event_invalid_slot()
elif 'InvalidGame' in errors:
ctx.event_invalid_game()
elif 'SlotAlreadyTaken' in errors:
raise Exception('Player slot already in use for that team')
elif 'IncompatibleVersion' in errors:
raise Exception('Server reported your client version as incompatible')
elif 'InvalidItemsHandling' in errors:
raise Exception('The item handling flags requested by the client are not supported')
# last to check, recoverable problem
elif 'InvalidPassword' in errors:
logger.error('Invalid password')
ctx.password = None
await ctx.server_auth(True)
elif errors:
raise Exception("Unknown connection errors: " + str(errors))
else:
raise Exception('Connection refused by the multiworld host, no reason provided')
elif cmd == 'Connected':
ctx.team = args["team"]
ctx.slot = args["slot"]
ctx.consume_players_package(args["players"])
msgs = []
if ctx.locations_checked:
msgs.append({"cmd": "LocationChecks",
"locations": list(ctx.locations_checked)})
if ctx.locations_scouted:
msgs.append({"cmd": "LocationScouts",
"locations": list(ctx.locations_scouted)})
if msgs:
await ctx.send_msgs(msgs)
if ctx.finished_game:
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
# Get the server side view of missing as of time of connecting.
# This list is used to only send to the server what is reported as ACTUALLY Missing.
# This also serves to allow an easy visual of what locations were already checked previously
# when /missing is used for the client side view of what is missing.
ctx.missing_locations = set(args["missing_locations"])
ctx.checked_locations = set(args["checked_locations"])
for ss in ctx.checked_locations:
filename = f"send{ss}"
with open(os.path.expandvars(r"%localappdata%/ChecksFinder/"+filename), 'w') as f:
f.close()
elif cmd == 'ReceivedItems':
start_index = args["index"]
if start_index == 0:
ctx.items_received = []
elif start_index != len(ctx.items_received):
sync_msg = [{'cmd': 'Sync'}]
if ctx.locations_checked:
sync_msg.append({"cmd": "LocationChecks",
"locations": list(ctx.locations_checked)})
await ctx.send_msgs(sync_msg)
if start_index == len(ctx.items_received):
for item in args['items']:
filename = f"AP_{str(NetworkItem(*item).location)}PLR{str(NetworkItem(*item).player)}.item"
with open(os.path.expandvars(r"%localappdata%/ChecksFinder/"+filename), 'w') as f:
f.write(str(NetworkItem(*item).item))
f.close()
ctx.items_received.append(NetworkItem(*item))
ctx.watcher_event.set()
elif cmd == 'LocationInfo':
for item, location, player in args['locations']:
if location not in ctx.locations_info:
ctx.locations_info[location] = (item, player)
ctx.watcher_event.set()
elif cmd == "RoomUpdate":
if "players" in args:
ctx.consume_players_package(args["players"])
if "hint_points" in args:
ctx.hint_points = args['hint_points']
if "checked_locations" in args:
checked = set(args["checked_locations"])
ctx.checked_locations |= checked
ctx.missing_locations -= checked
for ss in ctx.checked_locations:
filename = f"send{ss}"
with open(os.path.expandvars(r"%localappdata%/ChecksFinder/"+filename), 'w') as f:
f.close()
if "permissions" in args:
ctx.update_permissions(args["permissions"])
elif cmd == 'Print':
ctx.on_print(args)
elif cmd == 'PrintJSON':
ctx.on_print_json(args)
elif cmd == 'InvalidPacket':
logger.warning(f"Invalid Packet of {args['type']}: {args['text']}")
elif cmd == "Bounced":
tags = args.get("tags", [])
# we can skip checking "DeathLink" in ctx.tags, as otherwise we wouldn't have been send this
if "DeathLink" in tags and ctx.last_death_link != args["data"]["time"]:
ctx.on_deathlink(args["data"])
elif cmd == "SetReply":
if args["key"] == "EnergyLink":
ctx.current_energy_link_value = args["value"]
if ctx.ui:
ctx.ui.set_new_energy_link_value()
else:
logger.debug(f"unknown command {cmd}")
ctx.on_package(cmd, args)
async def game_watcher(ctx: CommonContext):
from worlds.checksfinder.Locations import lookup_id_to_name
while not ctx.exit_event.is_set():
if ctx.syncing == True:
sync_msg = [{'cmd': 'Sync'}]
if ctx.locations_checked:
sync_msg.append({"cmd": "LocationChecks", "locations": list(ctx.locations_checked)})
await ctx.send_msgs(sync_msg)
ctx.syncing = False
path = os.path.expandvars(r"%localappdata%/ChecksFinder")
sending = []
victory = False
for root, dirs, files in os.walk(path):
for file in files:
if file.find("send") > -1:
st = file.split("send", -1)[1]
sending = sending+[(int(st))]
if file.find("victory") > -1:
victory = True
ctx.locations_checked = sending
message = [{"cmd": 'LocationChecks', "locations": sending}]
await ctx.send_msgs(message)
if not ctx.finished_game and victory:
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
ctx.finished_game = True
await asyncio.sleep(0.1)
if __name__ == '__main__':
# Text Mode to use !hint and such with games that have no text entry
class TextContext(CommonContext):
game = "ChecksFinder"
items_handling = 0b111 # full remote
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
await super(TextContext, self).server_auth(password_requested)
if not self.auth:
logger.info('Enter slot name:')
self.auth = await self.console_input()
await self.send_connect()
def on_package(self, cmd: str, args: dict):
if cmd == "Connected":
self.game = self.games.get(self.slot, None)
async def main(args):
ctx = TextContext(args.connect, args.password)
ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop")
input_task = None
if gui_enabled:
from kvui import ChecksFinderManager
ctx.ui = ChecksFinderManager(ctx)
ui_task = asyncio.create_task(ctx.ui.async_run(), name="UI")
else:
ui_task = None
if sys.stdin:
input_task = asyncio.create_task(console_loop(ctx), name="Input")
progression_watcher = asyncio.create_task(
game_watcher(ctx), name="ChecksFinderProgressionWatcher")
await ctx.exit_event.wait()
ctx.server_address = None
await progression_watcher
await ctx.shutdown()
if ui_task:
await ui_task
if input_task:
input_task.cancel()
import colorama
parser = get_base_parser(description="ChecksFinder Client, for text interfacing.")
args, rest = parser.parse_known_args()
colorama.init()
loop = asyncio.get_event_loop()
loop.run_until_complete(main(args))
loop.close()
colorama.deinit()

View File

@ -18,6 +18,7 @@ Currently, the following games are supported:
* VVVVVV
* Raft
* Super Mario 64
* ChecksFinder
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled

View File

@ -0,0 +1,23 @@
# ChecksFinder
## Where is the settings page?
The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
config file.
## What is considered a location check in ChecksFinder?
Location checks in are completed when the player finds a spot on a board that has the archipelago logo. The bottom of
the screen has a number next to the archipelago logo, that number is how many you can find so far. You can only get as
many checks as you have gained items, plus one to start with being available.
## When the player receives an item, what happens?
When the player receives an item in ChecksFinder, it either can map the future boards they play be bigger in width or
height, or add a new bomb to the future boards, with a limit to having up to one fifth of the _current_ board being
bombs.
## What is the victory condition?
Victory is achieved when the player wins a board they were given after they have received all of their Map Width, Map
Height, and Map Bomb items. The game will say at the bottom of the screen how many of each you have received.

View File

@ -0,0 +1,45 @@
# ChecksFinder Randomizer Setup Guide
## Required Software
- ChecksFinder from
the [Github releases Page for the game](https://github.com/jonloveslegos/ChecksFinder/releases) (latest version)
- Archipelago from the [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases)
- (select `ChecksFinder Client` during installation.)
## Configuring your YAML file
### What is a YAML file and why do I need one?
See the guide on setting up a basic YAML at the Archipelago setup
guide: [Basic Multiworld Setup Guide](/tutorial/archipelago/setup/en)
### Where do I get a YAML file?
You can customize your settings by visiting the [ChecksFinder Player Settings Page](/games/ChecksFinder/player-settings)
### Generating a ChecksFinder game
**ChecksFinder is meant to be played _alongside_ another game! You may not be playing it for long periods of time if
you play it with another person!**
When you join a multiworld game, you will be asked to provide your YAML file to whoever is hosting. Once that is done,
the host will provide you with either a link to download your data file, or with a zip file containing everyone's data
files. You do not have a file inside that zip though!
You need to start ChecksFinder client yourself, it is located within the Archipelago folder.
### Connect to the MultiServer
First start ChecksFinder.
Once both ChecksFinder and the client are started. In the client at the top type in the spot labeled `Server` type the
`Ip Address` and `Port` separated with a `:` symbol.
The client will then ask for the username you chose, input that in the text box at the bottom of the client.
### Play the game
When the console tells you that you have joined the room, you're all set. Congratulations on successfully joining a
multiworld game!

View File

@ -517,5 +517,24 @@
]
}
]
},
{
"gameTitle": "ChecksFinder",
"tutorials": [
{
"name": "Multiworld Setup Tutorial",
"description": "A guide to setting up the Archipelago ChecksFinder software on your computer. This guide covers single-player, multiworld, and related software.",
"files": [
{
"language": "English",
"filename": "checksfinder/checksfinder_en.md",
"link": "checksfinder/checksfinder/en",
"authors": [
"Mewlif"
]
}
]
}
]
}
]

View File

@ -378,6 +378,13 @@ class FactorioManager(GameManager):
base_title = "Archipelago Factorio Client"
class ChecksFinderManager(GameManager):
logging_pairs = [
("Client", "Archipelago")
]
base_title = "Archipelago ChecksFinder Client"
class SNIManager(GameManager):
logging_pairs = [
("Client", "Archipelago"),

View File

@ -94,6 +94,8 @@ scripts = {
"OoTAdjuster.py": ("ArchipelagoOoTAdjuster", True, icon),
# FF1
"FF1Client.py": ("ArchipelagoFF1Client", True, icon),
# ChecksFinder
"ChecksFinderClient.py": ("ArchipelagoChecksFinderClient", True, icon),
}
exes = []

View File

@ -0,0 +1,27 @@
from BaseClasses import Item
import typing
class ItemData(typing.NamedTuple):
code: typing.Optional[int]
progression: bool
class ChecksFinderItem(Item):
game: str = "ChecksFinder"
item_table = {
"Map Width": ItemData(80000, True),
"Map Height": ItemData(80001, True),
"Map Bombs": ItemData(80002, True),
}
required_items = {
}
item_frequencies = {
}
lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in item_table.items() if data.code}

View File

@ -0,0 +1,52 @@
from BaseClasses import Location
import typing
class AdvData(typing.NamedTuple):
id: typing.Optional[int]
region: str
class ChecksFinderAdvancement(Location):
game: str = "ChecksFinder"
def __init__(self, player: int, name: str, address: typing.Optional[int], parent):
super().__init__(player, name, address, parent)
self.event = not address
advancement_table = {
"Tile 1": AdvData(81000, 'Board'),
"Tile 2": AdvData(81001, 'Board'),
"Tile 3": AdvData(81002, 'Board'),
"Tile 4": AdvData(81003, 'Board'),
"Tile 5": AdvData(81004, 'Board'),
"Tile 6": AdvData(81005, 'Board'),
"Tile 7": AdvData(81006, 'Board'),
"Tile 8": AdvData(81007, 'Board'),
"Tile 9": AdvData(81008, 'Board'),
"Tile 10": AdvData(81009, 'Board'),
"Tile 11": AdvData(81010, 'Board'),
"Tile 12": AdvData(81011, 'Board'),
"Tile 13": AdvData(81012, 'Board'),
"Tile 14": AdvData(81013, 'Board'),
"Tile 15": AdvData(81014, 'Board'),
"Tile 16": AdvData(81015, 'Board'),
"Tile 17": AdvData(81016, 'Board'),
"Tile 18": AdvData(81017, 'Board'),
"Tile 19": AdvData(81018, 'Board'),
"Tile 20": AdvData(81019, 'Board'),
"Tile 21": AdvData(81020, 'Board'),
"Tile 22": AdvData(81021, 'Board'),
"Tile 23": AdvData(81022, 'Board'),
"Tile 24": AdvData(81023, 'Board'),
"Tile 25": AdvData(81024, 'Board'),
}
exclusion_table = {
}
events_table = {
}
lookup_id_to_name: typing.Dict[int, str] = {data.id: item_name for item_name, data in advancement_table.items() if data.id}

View File

@ -0,0 +1,6 @@
import typing
from Options import Option
checksfinder_options: typing.Dict[str, type(Option)] = {
}

View File

@ -0,0 +1,16 @@
def link_checksfinder_structures(world, player):
for (exit, region) in mandatory_connections:
world.get_entrance(exit, player).connect(world.get_region(region, player))
# (Region name, list of exits)
checksfinder_regions = [
('Menu', ['New Board']),
('Board',[]),
]
# (Entrance, region pointed to)
mandatory_connections = [
('New Board', 'Board'),
]

View File

@ -0,0 +1,47 @@
from ..generic.Rules import set_rule, add_rule
from BaseClasses import MultiWorld
from ..AutoWorld import LogicMixin
class ChecksFinderLogic(LogicMixin):
def _has_total(self, player: int, total: int):
return (self.item_count('Map Width', player)+self.item_count('Map Height', player)+
self.item_count('Map Bombs', player)) >= total
# Sets rules on entrances and advancements that are always applied
def set_rules(world: MultiWorld, player: int):
set_rule(world.get_location(("Tile 6"), player), lambda state: state._has_total(player, 1))
set_rule(world.get_location(("Tile 7"), player), lambda state: state._has_total(player, 2))
set_rule(world.get_location(("Tile 8"), player), lambda state: state._has_total(player, 3))
set_rule(world.get_location(("Tile 9"), player), lambda state: state._has_total(player, 4))
set_rule(world.get_location(("Tile 10"), player), lambda state: state._has_total(player, 5))
set_rule(world.get_location(("Tile 11"), player), lambda state: state._has_total(player, 6))
set_rule(world.get_location(("Tile 12"), player), lambda state: state._has_total(player, 7))
set_rule(world.get_location(("Tile 13"), player), lambda state: state._has_total(player, 8))
set_rule(world.get_location(("Tile 14"), player), lambda state: state._has_total(player, 9))
set_rule(world.get_location(("Tile 15"), player), lambda state: state._has_total(player, 10))
set_rule(world.get_location(("Tile 16"), player), lambda state: state._has_total(player, 11))
set_rule(world.get_location(("Tile 17"), player), lambda state: state._has_total(player, 12))
set_rule(world.get_location(("Tile 18"), player), lambda state: state._has_total(player, 13))
set_rule(world.get_location(("Tile 19"), player), lambda state: state._has_total(player, 14))
set_rule(world.get_location(("Tile 20"), player), lambda state: state._has_total(player, 15))
set_rule(world.get_location(("Tile 21"), player), lambda state: state._has_total(player, 16))
set_rule(world.get_location(("Tile 22"), player), lambda state: state._has_total(player, 17))
set_rule(world.get_location(("Tile 23"), player), lambda state: state._has_total(player, 18))
set_rule(world.get_location(("Tile 24"), player), lambda state: state._has_total(player, 19))
set_rule(world.get_location(("Tile 25"), player), lambda state: state._has_total(player, 20))
# Sets rules on completion condition
def set_completion_rules(world: MultiWorld, player: int):
width_req = 10-5
height_req = 10-5
bomb_req = 20-5
completion_requirements = lambda state: \
state.has("Map Width", player, width_req) and \
state.has("Map Height", player, height_req) and \
state.has("Map Bombs", player, bomb_req)
world.completion_condition[player] = lambda state: completion_requirements(state)

View File

@ -0,0 +1,90 @@
import os
import json
from base64 import b64encode, b64decode
from math import ceil
from .Items import ChecksFinderItem, item_table, required_items
from .Locations import ChecksFinderAdvancement, advancement_table, exclusion_table
from .Regions import checksfinder_regions, link_checksfinder_structures
from .Rules import set_rules, set_completion_rules
from worlds.generic.Rules import exclusion_rules
from BaseClasses import Region, Entrance, Item
from .Options import checksfinder_options
from ..AutoWorld import World
client_version = 7
class ChecksFinderWorld(World):
"""
ChecksFinder is a game where you avoid mines and find checks inside the board
with the mines! You win when you get all your items and beat the board!
"""
game: str = "ChecksFinder"
options = checksfinder_options
topology_present = True
item_name_to_id = {name: data.code for name, data in item_table.items()}
location_name_to_id = {name: data.id for name, data in advancement_table.items()}
data_version = 4
def _get_checksfinder_data(self):
return {
'world_seed': self.world.slot_seeds[self.player].getrandbits(32),
'seed_name': self.world.seed_name,
'player_name': self.world.get_player_name(self.player),
'player_id': self.player,
'client_version': client_version,
'race': self.world.is_race,
}
def generate_basic(self):
# Generate item pool
itempool = []
# Add all required progression items
for (name, num) in required_items.items():
itempool += [name] * num
# Add the map width and height stuff
itempool += ["Map Width"] * (10-5)
itempool += ["Map Height"] * (10-5)
# Add the map bombs
itempool += ["Map Bombs"] * (20-5)
# Convert itempool into real items
itempool = [item for item in map(lambda name: self.create_item(name), itempool)]
# Choose locations to automatically exclude based on settings
exclusion_pool = set()
self.world.itempool += itempool
def set_rules(self):
set_rules(self.world, self.player)
set_completion_rules(self.world, self.player)
def create_regions(self):
def ChecksFinderRegion(region_name: str, exits=[]):
ret = Region(region_name, None, region_name, self.player, self.world)
ret.locations = [ChecksFinderAdvancement(self.player, loc_name, loc_data.id, ret)
for loc_name, loc_data in advancement_table.items()
if loc_data.region == region_name]
for exit in exits:
ret.exits.append(Entrance(self.player, exit, ret))
return ret
self.world.regions += [ChecksFinderRegion(*r) for r in checksfinder_regions]
link_checksfinder_structures(self.world, self.player)
def fill_slot_data(self):
slot_data = self._get_checksfinder_data()
for option_name in checksfinder_options:
option = getattr(self.world, option_name)[self.player]
if slot_data.get(option_name, None) is None and type(option.value) in {str, int}:
slot_data[option_name] = int(option.value)
return slot_data
def create_item(self, name: str) -> Item:
item_data = item_table[name]
item = ChecksFinderItem(name, item_data.progression, item_data.code, self.player)
return item