(WIP) MineWeeper Lobby
A Linkspace Application Tutorial
#!/bin/env python3
from dataclasses import dataclass,field
from typing import Optional,Dict,List,Tuple # compatibility: python3.8 does not support newer type hints.
import json
import os,functools,logging,sys
logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
from linkspace import *
group_name = os.environ.get("LK_GROUP","[#:test]")
group = lk_eval(group_name)
lk = lk_open()
key = lk_key(lk)
common_q = lk_query_parse(lk_query(),"domain:=:mineweeper","group:=:"+group_name)
keypoint = functools.partial(lk_keypoint,key=key,domain=b"mineweeper",group=group)
recent_ok = lk_status_poll(
lk,qid=b"status",callback=lambda _ : True,timeout=lk_eval("[us:+2s]"),
domain=b"exchange",
group=group,
objtype=b"process")
if not recent_ok and lk_process_while(lk,qid=b"status") == 0:
exit("No exchange process active. ")
@dataclass
class Lobby:
# empty keypoint at /host/NAME (signed by host)
create_pkt: Optional[Pkt] = None
# keypoint at /host/NAME with a link to create + the game config start data
start_pkt : Optional[Pkt] = None
# keypoints(signed by player) at /lobby/[create.hash] Data is player name
# (leaving is done by overwriting the keypoint without data
players:Dict[bytes,Pkt] = field(default_factory=dict)
# keypoints at /lobby/[create.hash]/chat to message each other.
chat:List[Pkt] = field(default_factory=list)
print("Welcome to the mineweeper lobby. Pick a game or setup a new one.")
print("Press enter to refresh or enter 'w' to await for change")
# Because we'll be using 'input()' we dont't have asynchronous feedback.
# In a better program we would use a thread for the user interface.
get_lobbies = lk_query_parse(common_q,"prefix:=:/host","depth:=:[u8:2]","create:>:[now:-10m]",":qid:lobbies")
lk_pull(lk,lk_query_parse(get_lobbies,":follow"))
host_lobby = None
lobbies:Dict[Tuple[bytes,bytes],Lobby] = dict()
def insert(p:Pkt):
lobby = lobbies.get((p.comp1,p.pubkey),Lobby())
if not len(p.links):
lobby.create_pkt = p
else:
lobby.start_pkt = p
lobbies[(p.comp1,p.pubkey)] = lobby
lk_watch(lk,get_lobbies,insert)
lk_process(lk);
while host_lobby is None or host_lobby.create_pkt is None:
try:
opts = [((b"New Game",key.pubkey),Lobby())] + [l for l in lobbies.items() if l[1].start_pkt is None]
for i,((name,pubkey),lobby) in enumerate(opts):
print(f"{i})",name.decode("utf-8"),lk_encode(key.pubkey,"@/b"))
i = input("<int> to join > ");
if i == "w":
lk_process_while(lk,qid=b"lobbies",timeout=lk_eval("[us:+10s]"))
continue
if i == "":
lk_process(lk)
continue
i = int(i)
if i == 0:
game_name = ""
while not game_name:
game_name = input("Game name > ")
host_space = lk_eval("[//host/[0]]",argv=[game_name])
create = keypoint(space=host_space)
lk_save(lk,create)
lk_process(lk)
host_lobby = lobbies[(create.comp1,create.pubkey)]
else:
lobby = opts[i]
host_lobby = lobby[1]
except Exception as e :
print(e)
print("Using lobby",host_lobby.create_pkt.comp1.decode("utf-8"), " designated admin: ",lk_encode(host_lobby.create_pkt.pubkey,"@/b"))
player_name = ""
while not player_name:
player_name = input("Player name > ")
get_lobby = lk_query_parse(
common_q,"prefix:=:/lobby/[hash]",":qid:lobby[hash]",
pkt=host_lobby.create_pkt)
# we could have used lk_query_push(common_q,"","qid",b"lobby" + host.create_pkt.hash)
def lobby_pkt(p:Pkt):
global host_lobby
if p.comp2 == b"chat":
host_lobby.chat.append(p)
elif p.comp2 == b"":
latest = host_lobby.players.get(p.pubkey)
if latest and latest.create > p.create:
return
host_lobby.players[p.pubkey] = p
else:
print("unknown msg",p)
lobby_space = lk_eval("[//lobby/[hash]]",pkt=host_lobby.create_pkt)
join_pkt = keypoint(space=lobby_space,data=player_name)
# Its important to understand we used the hash bytes directly.
# There is no encoding to b64.
# host.create_pkt.hash == join_pkt.comp1 and b64(host.create_pkt.hash) == b64(join_pkt.comp1)
lk_save(lk,join_pkt)
lk_watch(lk,get_lobby, on_match=lobby_pkt)
lk_process(lk) # Process ourselves
lk_pull(lk,get_lobby) # request more
me_host = host_lobby.create_pkt.pubkey == key.pubkey
prompt = "[chat msg] | /leave"
if me_host:
prompt += " | /start"
elif len(host_lobby.players) < 2: # we should wait for info on the existing players
lk_process_while(lk,qid=b"lobby"+host_lobby.create_pkt.hash)
while not host_lobby.start_pkt:
# Print the chat log
print(*[ p.data.decode("utf-8") for p in host_lobby.chat],sep="\n",end="\n")
# Print the player names
print("Players:", ",".join([ p.data.decode("utf-8") for p in host_lobby.players.values() if p.data]))
print(prompt)
try:
cmd = input("> ")
except KeyboardInterrupt:
cmd = "/leave"
if cmd == "":
#lk_process_while(lk,timeout=lk_eval("[us:+2s]"))
lk_process(lk)
elif me_host and cmd == "/start":
lk_process(lk)
players = [ [p.data.decode("utf-8"),b64(p.pubkey)] for p in host_lobby.players.values() if p.data]
print(*players,sep="\n")
try:
cols = int(input("cols (10) > ") or "10")
rows = int(input("rows (10) > ") or "10")
mine_rate = float(input("mine_rate (0.2) > ")or "0.2")
data = json.dumps({"columns":cols,"rows":rows,"mine_rate":mine_rate,"players":players})
start_pkt = keypoint(spacename=host_lobby.create_pkt.spacename,data=data,links=[Link("create",host_lobby.create_pkt.hash)])
lk_save(lk,start_pkt)
lk_process(lk)
except Exception as e:
print(e)
elif cmd == "/leave":
lk_save(lk,keypoint(space=lobby_space))
if me_host:
leave_pkt = keypoint(spacename=host_lobby.create_pkt.spacename,links=[Link("create",host_lobby.create_pkt.hash)]) # no data means we closed the lobby.
lk_save(lk,leave_pkt)
print("host left ",leave_pkt)
lk_process(lk)
exit("We left.")
else:
lk_save(lk,keypoint(space=lobby_space + lk_eval("[//chat]"),data=cmd))
lk_process(lk)
if not len(host_lobby.start_pkt.data):
exit("Host left.")
print("Starting game", host_lobby.start_pkt, host_lobby.start_pkt.data.decode("utf-8"))
# A real program would import "mineweeper-multiplayer" and just call a function to start the game.
# For the purpose of the tutorial, we keep the two scripts loosely coupled.
srcpath = os.path.dirname(os.path.realpath(__file__))
game_setup_hash = b64(host_lobby.start_pkt.hash)
os.system(f"python \"{srcpath}/mineweeper-multiplayer.py\" {game_setup_hash}")