<

(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_watch(
    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}")

Created: 2023-11-05 Sun 09:20