| 1 | import socketio |
| 2 | import eventlet |
| 3 | import sqlite3 |
| 4 | from re import match as re_match |
| 5 | from datetime import date |
| 6 | from os import getenv |
| 7 | from sys import path as sys_path |
| 8 | |
| 9 | # Create a Socket.IO server (CORS arg required on server, not locally) |
| 10 | MODE = getenv('RPSLS_MODE') |
| 11 | allowed_origin = 'https://rpsls.auder.net' if MODE=='production' else '*' |
| 12 | sio = socketio.Server(cors_allowed_origins=allowed_origin) |
| 13 | |
| 14 | RPSLS_PATH = sys_path[0] |
| 15 | DB_PATH = RPSLS_PATH + '/db/rpsls.sqlite' |
| 16 | |
| 17 | searching = {} #someone seeks a game? (uid + sid) |
| 18 | connected = {} #map uid --> sid (seek stage) |
| 19 | |
| 20 | # Avoid repeating DB connect/close code |
| 21 | def db_operation(func): |
| 22 | con = sqlite3.connect(DB_PATH) |
| 23 | cur = con.cursor() |
| 24 | func(cur) |
| 25 | con.commit() |
| 26 | con.close() |
| 27 | |
| 28 | @sio.event |
| 29 | def disconnect(sid): |
| 30 | """ Triggered at page reload or tab close """ |
| 31 | global connected, searching |
| 32 | try: |
| 33 | key_idx = list(connected.values()).index(sid) |
| 34 | del connected[list(connected.keys())[key_idx]] |
| 35 | except ValueError: |
| 36 | # If the user didn't seek, no key to find |
| 37 | pass |
| 38 | if searching and searching["sid"] == sid: |
| 39 | searching = {} |
| 40 | |
| 41 | @sio.event |
| 42 | def login(sid, data): |
| 43 | """ When user sends name from /login page """ |
| 44 | if not re_match(r"^[a-zA-Z]{3,}$", data): |
| 45 | sio.emit("login", {"err": "Name: letters only"}, room=sid) |
| 46 | return |
| 47 | def upsert(cur): |
| 48 | uid = 0 |
| 49 | try: |
| 50 | # Always try to insert (new) Users row |
| 51 | cur.execute("insert into Users (name) values (?)", (data,)) |
| 52 | uid = cur.lastrowid |
| 53 | except sqlite3.IntegrityError as err: |
| 54 | # If fails: user already exists, find its ID |
| 55 | if str(err) == "UNIQUE constraint failed: Users.name": |
| 56 | cur.execute("select id from Users where name = ?", (data,)) |
| 57 | uid = cur.fetchone()[0] |
| 58 | else: |
| 59 | raise |
| 60 | sio.emit("login", {"name": data, "uid": uid}, room=sid) |
| 61 | db_operation(upsert) |
| 62 | |
| 63 | @sio.event |
| 64 | def seek(sid, data): |
| 65 | """ When user click on 'Play' button """ |
| 66 | global connected, searching |
| 67 | connected[data["uid"]] = sid |
| 68 | if not searching: |
| 69 | searching = {"uid": data["uid"], "sid": sid, "name": data["name"]} |
| 70 | else: |
| 71 | # Active seek pending: create game |
| 72 | opponent = searching |
| 73 | searching = {} |
| 74 | def create_game(cur): |
| 75 | today = (date.today(),) |
| 76 | cur.execute("insert into Games (created) values (?)", today) |
| 77 | gid = cur.lastrowid |
| 78 | # To room == sid, opponent is me. To my room, it's him/her |
| 79 | sio.emit("play", |
| 80 | {"gid":gid, "oppid":opponent["uid"], "oppname":opponent["name"]}, |
| 81 | room=sid) |
| 82 | sio.emit("play", |
| 83 | {"gid":gid, "oppid":data["uid"], "oppname":data["name"]}, |
| 84 | room=opponent["sid"]) |
| 85 | id_list = [(data["uid"],gid), (opponent["uid"],gid)] |
| 86 | cur.executemany("insert into Players (uid,gid) values (?,?)", id_list) |
| 87 | db_operation(create_game) |
| 88 | |
| 89 | @sio.event |
| 90 | def move(sid, data): |
| 91 | """ New move to DB + transmit to opponent """ |
| 92 | sio.emit("move", data, room=connected[data["oppid"]]) |
| 93 | db_operation(lambda cur: |
| 94 | cur.execute("insert into Moves (uid,gid,choice,mnum) values (?,?,?,?)", |
| 95 | (data["uid"],data["gid"],data["choice"],data["mnum"])) |
| 96 | ) |
| 97 | |
| 98 | @sio.event |
| 99 | def inc_pts(sid, data): |
| 100 | """ Add a point to the player (who won last round) """ |
| 101 | db_operation(lambda cur: |
| 102 | cur.execute("update Players set points=points+1 where uid=? and gid=?", |
| 103 | (data["uid"],data["gid"])) |
| 104 | ) |
| 105 | |
| 106 | static_files = { |
| 107 | '/': RPSLS_PATH + '/index.html', |
| 108 | '/rpsls.js': RPSLS_PATH + '/rpsls.js', |
| 109 | '/favicon.ico': RPSLS_PATH + '/favicon.ico', |
| 110 | '/assets': RPSLS_PATH + '/assets' |
| 111 | } |
| 112 | |
| 113 | PORT = getenv('RPSLS_PORT') |
| 114 | if PORT is None: |
| 115 | PORT = "8000" |
| 116 | PORT = int(PORT) |
| 117 | |
| 118 | # Wrap with a WSGI application |
| 119 | app = socketio.WSGIApp(sio, static_files=static_files) |
| 120 | eventlet.wsgi.server(eventlet.listen(('127.0.0.1', PORT)), app) |