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