From 3c1a85153d57328182de9b31e1d4f93c62463d90 Mon Sep 17 00:00:00 2001 From: Ivan Mahonin Date: Jan 06 2023 19:09:02 +0000 Subject: initial commit --- diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a60a986 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/__pycache__ +/cards/* +/words/* +!/cards/.placeholder +!/words/.placeholder +!/words/words.txt.example +config.py diff --git a/cards/.placeholder b/cards/.placeholder new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/cards/.placeholder diff --git a/config.py.example b/config.py.example new file mode 100644 index 0000000..212f890 --- /dev/null +++ b/config.py.example @@ -0,0 +1,9 @@ + +hostName = "127.0.0.1" +serverPort = 8080 +externalHost = "http://" + hostName + ":" + str(serverPort) +prefix = "/pixit" +cardsPrefix = prefix + "/cards" + +cardsCount = 7 + diff --git a/game.py b/game.py new file mode 100644 index 0000000..e33f968 --- /dev/null +++ b/game.py @@ -0,0 +1,136 @@ + +import config + +import os +import uuid +import random + + +class Player: + def __init__(self, game, name): + self.game = game + self.name = name + self.id = str(uuid.uuid4()) + self.beginturn() + players[self.id] = self + + def beginturn(self): + self.word = "" + self.ready = False + self.selection = -1 + + def select(self, i): + if self.selection < 0 and i >= 0 and i < len(self.game.cards): + self.selection = i + self.game.playerSelect() + + def nextturn(self, word): + if self.game.selected and not self.ready: + self.word = str(word).strip() + self.ready = True + self.game.playerReady() + + def status(self): + friend = self.game.players[1] if self.game.players[0] == self else self.game.players[0] + friendId = friend.id if self.game.players[0] == self else None + friendSelection = friend.selection if self.game.selected else -1 + cards = [] + for i in range(0, len(self.game.cards)): + cards.append({ "index": i, "name": self.game.cards[i], "my": self.selection == i, "friends": friendSelection == i }) + return { + "turn": self.game.turn, + "score": self.game.score, + "word": self.game.word, + "wordSource": self.game.wordSource, + "cards": cards, + "selected": self.game.selected, + "host": config.externalHost, + "prefix": config.prefix, + "cardsPrefix": config.cardsPrefix, + "waiting": (self.selection >= 0 and friendSelection < 0) or (self.ready and not friend.ready), + "win": self.game.selected and self.selection == friend.selection, + "lose": self.game.selected and self.selection != friend.selection, + "me": { + "id": self.id, + "name": self.name, + "selection": self.selection, + "selected": self.selection >= 0, + "ready": self.ready }, + "friend": { + "id": friendId, + "selection": friendSelection } } + + +class Game: + def __init__(self): + self.players = ( Player(self, "Player1"), Player(self, "Player2") ) + self.turn = 1 + self.score = 0 + self.cards = [] + self.beginturn() + + def beginturn(self): + assert(cards) + self.cards.clear() + for i in range(0, config.cardsCount): + for j in range(0, 100): + card = cards[ random.randrange(0, len(cards)) ] + if not card in self.cards: + self.cards.append(card) + break + w0 = self.players[0].word + w1 = self.players[1].word + if not w0 and not w1: + self.word = random.choice(words) + self.wordSource = "choosen by random" + elif not w1: + self.word = w0 + self.wordSource = "suggested by player1" + elif not w0: + self.word = w1 + self.wordSource = "suggested by player2" + elif w0 == w1: + self.word = w0 + self.wordSource = "suggested by both players" + elif random.randrange(0, 1): + self.word = w0 + self.wordSource = "suggested by player1 (player2 may be lucky next time)" + else: + self.word = w1 + self.wordSource = "suggested by player2 (player1 may be lucky next time)" + self.players[0].beginturn() + self.players[1].beginturn() + self.selected = False + + def playerSelect(self): + if not self.selected and self.players[0].selection >= 0 and self.players[1].selection >= 0: + self.selected = True + if self.players[0].selection == self.players[1].selection: + self.score = self.score + 1 + + def playerReady(self): + if self.selected and self.players[0].ready and self.players[1].ready: + self.turn = self.turn + 1 + self.beginturn() + + +def mergeCards(path): + for f in os.scandir(path): + if f.is_file() and f.name.endswith(".jpg"): + cards.append(f.name) + +def mergeWords(path): + global words + ws = set(words) + with open(path, "r") as f: + for word in f.readlines(): + w = word.strip() + if w != "": + ws.add(w) + words.clear() + words += ws + +players = {} +cards = [] +words = [] + diff --git a/server.py b/server.py new file mode 100755 index 0000000..b6283c7 --- /dev/null +++ b/server.py @@ -0,0 +1,152 @@ +#!/usr/bin/python3 + +import config +import game +import template + +import os +import json +import threading +import http.server +import urllib.parse + + +class Server(http.server.BaseHTTPRequestHandler): + def write(self, *texts): + for text in texts: + self.wfile.write(bytes(str(text), "utf8")) + + def parsePath(self, path): + if not path.startswith(config.prefix): + return None + path = path[len(config.prefix):] + if len(path) and path[0] != '/': + return None + return list(filter(None, path.split("/"))) + + def err(self, code = 404, message = ""): + self.send_response(int(code)) + self.send_header("Content-type", "text/html") + self.end_headers() + self.write(str(code) + " " + str(message)); + + def writeJpeg(self, path): + if not path.endswith(".jpg") or not os.path.isfile(path): + return self.err() + with open(path, "rb") as f: + self.send_response(200) + self.send_header("Content-type", "image/jpeg") + self.send_header("Cache-Control", "max-age=3600") + self.end_headers() + self.wfile.write(f.read()) + + def writeStatus(self, player): + self.send_response(200) + self.send_header("Content-type", "text/html") + self.end_headers() + self.write(json.dumps(player.status())) + + def do_POST(self): + if self.command != 'POST': + return self.err() + path = self.parsePath(self.path) + if path is None: + return self.err() + + length = int(self.headers.get('content-length')) + data = self.rfile.read(length) + fields = urllib.parse.parse_qs(str(data,"UTF-8")) + + with mutex: + if not path: # create new game + g = game.Game() + self.send_response(303) + self.send_header("Location", str(config.prefix) + "/" + str(g.players[0].id)) + self.end_headers() + return + + player = game.players.get(path[0], None) + if not player: + return self.err() + del path[0] + + if len(path) == 2 and path[0] == "select": + i = -1 + try: + i = int(path[1]) + except ValueError: + return self.err() + player.select(i) + elif len(path) == 1 and path[0] == "ready": + word = str(fields.get("word", [""])[0]) + player.nextturn(word) + else: + return self.err() + + self.send_response(303) + self.send_header("Location", str(config.prefix) + "/" + str(player.id)) + self.end_headers() + + def do_GET(self): + if self.command != 'GET': + return self.err() + + if self.path.startswith(config.cardsPrefix): + p = str(self.path[len(config.cardsPrefix):]) + if p and p[0] == "/": + if p.count("/") > 1 or p.count("..") > 0: + return self.err() + return self.writeJpeg("cards" + p) + + path = self.parsePath(self.path) + if path is None: + return self.err() + + if not path: + self.send_response(200) + self.send_header("Content-type", "text/html") + self.end_headers() + tplStartpage.write(self, {"host": config.externalHost, "prefix": config.prefix}); + return + + with mutex: + if len(path) != 1: + return self.err() + j = False + if path[0].endswith(".json"): + j = True + path[0] = path[0][0:-5] + player = game.players.get(path[0], None) + if not player: + return self.err() + if j: + return self.writeStatus(player) + self.send_response(200) + self.send_header("Content-type", "text/html") + self.end_headers() + tplPlayerpage = template.TplLoader.load("tpl/playerpage.tpl") + tplPlayerpage.write(self, player.status()); + + +mutex = threading.Lock() + +tplStartpage = template.TplLoader.load("tpl/startpage.tpl") +tplPlayerpage = template.TplLoader.load("tpl/playerpage.tpl") + +game.mergeCards("cards") +game.mergeWords("words/words.txt") + +webServer = http.server.HTTPServer(( + config.hostName, + config.serverPort ), Server ) + + +print("Server started http://%s:%s" % ( + config.hostName, + config.serverPort )) +try: + webServer.serve_forever() +except KeyboardInterrupt: + pass +webServer.server_close() +print("Server stopped.") diff --git a/template.py b/template.py new file mode 100644 index 0000000..49b38ea --- /dev/null +++ b/template.py @@ -0,0 +1,417 @@ + +import os +import html +import markdown +os.path.dirname(os.path.abspath(__file__)) + + +def dictGet(d, path): + for k in str(path).split('.'): + if type(d) is list or type(d) is tuple: + d = d[int(k)] + continue + elif type(d) is dict: + if (not k in d) and ('ex' in d): + print("query ex for field: %s (%s)" % (path, k)) + d = d['ex'](d) + if k in d: + d = d[k] + continue + print("variable not found: %s (%s)" % (path, k)) + return None + return d + +def tostr(s): + return '' if s is None else str(s) +def text2html(t): + return html.escape(tostr(t)) +def md2html(t): + return markdown.markdown(tostr(t)) + + +tabsym = ' -- ' + + +class TemplateText: + def __init__(self, text = ''): + self.text = text + def write(self, writer, context): + writer.write(self.text) + def optimize(self): + return self if self.text else None + def dbgPrint(self, tab): + print("%stext(%s)" % (tab, self.text.replace("\n", "\\n"))) + +class TemplateVarHtml: + def __init__(self, varpath = None): + self.varpath = varpath + def write(self, writer, context): + writer.write(tostr(dictGet( context, self.varpath ))) + def optimize(self): + return self + def dbgPrint(self, tab): + print("%svarHtml(%s)" % (tab, self.varpath)) + +class TemplateVarMd: + def __init__(self, varpath = None): + self.varpath = varpath + def write(self, writer, context): + writer.write(md2html(dictGet( context, self.varpath ))) + def optimize(self): + return self + def dbgPrint(self, tab): + print("%svarMd(%s)" % (tab, self.varpath)) + +class TemplateVarText: + def __init__(self, varpath = None): + self.varpath = varpath + def write(self, writer, context): + writer.write(text2html(dictGet( context, self.varpath ))) + def optimize(self): + return self + def dbgPrint(self, tab): + print("%svarText(%s)" % (tab, self.varpath)) + +class TemplateVarCount: + def __init__(self, varpath = None): + self.varpath = varpath + def write(self, writer, context): + v = dictGet( context, self.varpath ) + writer.write(str(len(v) if hasattr(v, '__len__') else 0)) + def optimize(self): + return self + def dbgPrint(self, tab): + print("%svarCount(%s)" % (tab, self.varpath)) + +class TemplateVarField: + def __init__(self, varpath = None): + self.varpath = varpath + def write(self, writer, context): + f = self.varpath.split('.')[-1] + writer.write("

%s: %s

" % ( + text2html( f ), + text2html( dictGet(context, self.varpath) ) )) + def optimize(self): + return self + def dbgPrint(self, tab): + print("%svarField(%s)" % (tab, self.varpath)) + +class TemplateIf: + def __init__(self, varpath = None, sub = None, alt = None): + self.varpath = varpath + self.sub = sub + self.alt = alt + def write(self, writer, context): + if dictGet(context, self.varpath): + if self.sub: self.sub.write(writer, context) + else: + if self.alt: self.alt.write(writer, context) + def optimize(self): + self.sub = self.sub.optimize() if self.sub else None + self.alt = self.alt.optimize() if self.alt else None + return self if self.sub or self.alt else None + def dbgPrint(self, tab): + print("%sif(%s):" % (tab, self.varpath)) + if self.sub: + self.sub.dbgPrint(tab + tabsym) + if self.alt: + print("%selse:" % tab) + self.alt.dbgPrint(tab + tabsym) + + +class TemplateWith: + def __init__(self, varpath = None, varname = None, sub = None): + self.varpath = varpath + self.varname = varname + self.sub = sub + def write(self, writer, context): + if self.sub: + context = dict(context) + context[self.varname] = dictGet(context, self.varpath) + self.sub.write(writer, context) + def optimize(self): + self.sub = self.sub.optimize() if self.sub else None + return self if self.sub else None + def dbgPrint(self, tab): + print("%swith(%s:%s):" % (tab, self.varpath, self.varname)) + if self.sub: self.sub.dbgPrint(tab + tabsym) + + +class TemplateComment: + def __init__(self, text = None): + self.text = text + def write(self, writer, context): + pass + def optimize(self): + return None + def dbgPrint(self, tab): + print("%scomment(%s)" % (texttab, self.text)) + + +class TemplateLoop: + def __init__(self, varpath = None, keyvarname = None, varname = None, sub = None, sep = None): + self.varpath = varpath + self.keyvarname = keyvarname + self.varname = varname + self.sub = sub + self.sep = sep + def writeItem(self, writer, context, first, k, v): + if self.sep and not first: + self.sep.write(writer, context) + if self.sub: + ctx = dict(context) + ctx[self.keyvarname] = k + ctx[self.varname] = v + self.sub.write(writer, ctx) + def write(self, writer, context): + if self.sep or self.sub: + vv = dictGet(context, self.varpath) + if type(vv) is dict: + f = True + keys = list(vv.keys()) + keys.sort() + for k in keys: + self.writeItem(writer, context, f, k, vv[k]) + f = False + elif hasattr(vv, '__iter__'): + idx = 0 + for v in vv: + self.writeItem(writer, context, not idx, idx, v) + idx = idx + 1 + def optimize(self): + self.sub = self.sub.optimize() if self.sub else None + self.sep = self.sep.optimize() if self.sep else None + return self if self.sub or self.sep else None + def dbgPrint(self, tab): + print("%sloop(%s:%s:%s):" % (tab, self.varpath, self.keyvarname, self.varname)) + if self.sub: + self.sub.dbgPrint(tab + tabsym) + if self.sep: + print("%ssep:" % tab) + self.sep.dbgPrint(tab + tabsym) + +class Template: + def __init__(self): + self.items = [] + def write(self, writer, context): + for i in self.items: + i.write(writer, context) + def optimize(self): + it = [] + for i in self.items: + ii = i.optimize() + if ii: + if it and type(ii) is TemplateText and type(it[-1]) is TemplateText: + it[-1].text = it[-1].text + ii.text + else: + it.append(ii) + self.items = it + if len(self.items) == 1: return self.items[0] + return self if self.items else None + def dbgPrint(self, tab): + print("%stemplate:" % tab) + for i in self.items: + i.dbgPrint(tab + tabsym) + + +class TplLoader: + @staticmethod + def loadtext(filename): + filename = os.path.abspath(filename) + path = os.path.dirname(filename) + res = '' + text = '' + with open(filename, 'r') as f: text = f.read() + ii = 0 + i = 0 + while i < len(text): + if text[i:i+9] == '{include:': + res = res + text[ii:i] + a = i+9 + b = text.find('}', a) + if b<0: raise Exception("include parse error: %s:%d" % (filename, a)) + fn = text[a:b] + res = res + TplLoader.loadtext(path +'/' + fn) + i = b+1 + ii = i + else: + i = i + 1 + return res + text[ii:i] + + def __init__(self, filename): + self.filename = os.path.abspath(filename) + self.text = TplLoader.loadtext(self.filename) + self.ptr = 0 + + def error(self): + with open('tplerr.txt', 'w') as f: f.write(self.text) + raise Exception("parse error: %s:%d" % (self.filename, self.ptr)) + + def readKey(self, k): + if self.text[self.ptr:self.ptr + len(k)] == k: + self.ptr = self.ptr + len(k) + return True + return False + + def readVarname(self): + i = self.ptr + while i < len(self.text) and (self.text[i].isalnum() or self.text[i] in '_'): + i = i + 1 + if i == self.ptr: return None + i, self.ptr = self.ptr, i + return self.text[i:self.ptr] + + def readVarpath(self): + i = self.ptr + varpath = None + while True: + varname = self.readVarname() + if not varname: + if varpath: self.error() + break + varpath = varpath + '.' + varname if varpath else varname + if not self.readKey('.'): break + if not varpath: + self.ptr = i + return varpath + + def loadVarText(self): + if not self.readKey("{:"): return None + varpath = self.readVarpath() + if not varpath or not self.readKey("}"): self.error() + return TemplateVarText(varpath) + + def loadVarHtml(self): + if not self.readKey("{html:"): return None + varpath = self.readVarpath() + if not varpath or not self.readKey("}"): self.error() + return TemplateVarHtml(varpath) + + def loadVarMd(self): + if not self.readKey("{md:"): return None + varpath = self.readVarpath() + if not varpath or not self.readKey("}"): self.error() + return TemplateVarMd(varpath) + + def loadVarCount(self): + if not self.readKey("{count:"): return None + varpath = self.readVarpath() + if not varpath or not self.readKey("}"): self.error() + return TemplateVarCount(varpath) + + def loadVarField(self): + if not self.readKey("{f:"): return None + varpath = self.readVarpath() + if not varpath or not self.readKey("}"): self.error() + return TemplateVarField(varpath) + + def loadIf(self): + if not self.readKey("{if:"): return None + varpath = self.readVarpath() + if not varpath or not self.readKey("}"): self.error() + res = TemplateIf(varpath, Template(), Template()) + e = False + while True: + if self.readKey("{endif}"): break + if self.readKey("{else}"): + if e: self.error() + e = True + continue + item = self.loadTemplate() + if not item: self.error() + if e: + res.alt.items.append(item) + else: + res.sub.items.append(item) + return res + + def loadWith(self): + if not self.readKey("{with:"): return None + varpath = self.readVarpath() + if not varpath or not self.readKey(":"): self.error() + varname = self.readVarname() + if not varname or not self.readKey("}"): self.error() + res = TemplateWith(varpath, varname, Template()) + while True: + if self.readKey("{endwith}"): break + item = self.loadTemplate() + if not item: self.error() + res.sub.items.append(item) + return res + + def loadComment(self): + if not self.readKey("{comment}"): return None + res = TemplateComment() + i = self.text.find('{endcomment}', self.ptr) + if i < 0: + i = len(self.text) + res = TemplateComment(self.text[self.ptr:i]) + self.ptr = i + if not self.readKey("{endcomment}"): self.error() + return res + + def loadLoop(self): + if not self.readKey("{loop:"): return None + varpath = self.readVarpath() + if not varpath or not self.readKey(":"): self.error() + keyvarname = self.readVarname() + if not self.readKey(":"): self.error() + varname = self.readVarname() + if not varname or not self.readKey("}"): self.error() + res = TemplateLoop(varpath, keyvarname, varname, Template(), Template()) + s = False + while True: + if self.readKey("{endloop}"): break + if self.readKey("{sep}"): + if s: self.error() + s = True + continue + item = self.loadTemplate() + if not item: self.error() + if s: + res.sep.items.append(item) + else: + res.sub.items.append(item) + return res + + def loadTemplate(self): + t = self.loadVarText() + if t: return t + t = self.loadVarHtml() + if t: return t + t = self.loadVarMd() + if t: return t + t = self.loadVarCount() + if t: return t + t = self.loadVarField() + if t: return t + t = self.loadIf() + if t: return t + t = self.loadWith() + if t: return t + t = self.loadComment() + if t: return t + t = self.loadLoop() + if t: return t + + i = self.text.find('{', self.ptr) + if i == self.ptr: + i = self.text.find('{', self.ptr+1) + if i < 0: + i = len(self.text) + if i <= self.ptr: + return None + i, self.ptr = self.ptr, i + return TemplateText(self.text[i:self.ptr]) + + @staticmethod + def load(filename): + l = TplLoader(filename) + t = Template() + while True: + tt = l.loadTemplate() + if not tt: break + t.items.append(tt) + if l.ptr < len(l.text): l.error() + t = t.optimize() + return t if t else TemplateText() diff --git a/tpl/css.tpl b/tpl/css.tpl new file mode 100644 index 0000000..6a1aa9c --- /dev/null +++ b/tpl/css.tpl @@ -0,0 +1,65 @@ + +body { font-size: 12px; text-align: center; } + +#logo { + font-size: 16px; + margin: 16px; +} + +#info { + font-size: 16px; + font-weight: bold; + margin-top: 18px; +} + +#word { } +#word h1 { font-size: 24px; font-weight: bold; margin: 18px 0 0 0; } + +#win { + font-size: 24px; + font-weight: bold; + margin: 18px; +} + +#lose { + font-size: 24px; + font-weight: bold; + margin: 18px; + color: gray; +} + + +#cards { + margin-bottom: 18px; +} + +.card { + display: inline-block; + border: 1px solid #aaaaaa; + border-radius: 16px; + margin: 8px; + padding: 8px; + width: 376px; +} + +.myCard { + background: #aaaaff; +} + +.friendsCard { + background: #ffaaaa; +} + +.myCard.friendsCard { + background: #aaffaa; +} + + +.card img { + width: 360px; + height: auto; +} + +#debug { display: none; } +#ready { } +#friendurl { } diff --git a/tpl/footer.tpl b/tpl/footer.tpl new file mode 100644 index 0000000..a1ed2f5 --- /dev/null +++ b/tpl/footer.tpl @@ -0,0 +1 @@ +{if:waiting}{endif} diff --git a/tpl/header.tpl b/tpl/header.tpl new file mode 100644 index 0000000..2d1c16a --- /dev/null +++ b/tpl/header.tpl @@ -0,0 +1,6 @@ + + dixit + + + {if:waiting}{endif} + diff --git a/tpl/playerpage.tpl b/tpl/playerpage.tpl new file mode 100644 index 0000000..e32f0fc --- /dev/null +++ b/tpl/playerpage.tpl @@ -0,0 +1,37 @@ +{include:header.tpl} +{if:friend.id}
URL for the friend: {:host}{:prefix}/{:friend.id}
{endif} +
{:me.name}, score: {:score}, turn: {:turn}
+
+

{:word}

+({:wordSource}) +
+ +{if:win} +
Answers are equal. You win!
+{endif} +{if:lose} +
Answers are different. You lose...
+{endif} + +{if:waiting} +
waiting for the friend
+{else}{if:selected} +
+ + +
+{endif}{endif} + +
my selection: {:me.selection}, friend's selection: {:friend.selection}
+ +
{loop:cards::card} +
+ +{if:me.selected}{else}
{endif} +
+{endloop}
+ +
+ + +{include:footer.tpl} diff --git a/tpl/startpage.tpl b/tpl/startpage.tpl new file mode 100644 index 0000000..db595c1 --- /dev/null +++ b/tpl/startpage.tpl @@ -0,0 +1,5 @@ +{include:header.tpl} +
+ +
+{include:footer.tpl} diff --git a/words/.placeholder b/words/.placeholder new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/words/.placeholder diff --git a/words/words.txt.example b/words/words.txt.example new file mode 100644 index 0000000..47c3851 --- /dev/null +++ b/words/words.txt.example @@ -0,0 +1,4 @@ +hello +world +good day +bye bye