diff --git a/.gitignore b/.gitignore index f0cc3fe..564f6f8 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,9 @@ env/ venv/ ENV/ env.bak/ -venv.bak/ \ No newline at end of file +venv.bak/ + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class \ No newline at end of file diff --git a/backend/README.md b/backend/README.md index 9c0064a..6ab13f7 100644 --- a/backend/README.md +++ b/backend/README.md @@ -37,3 +37,4 @@ Make sure to respect the [issue-template](../.github/issue_template.md) and the ### Learn More - Check out the [socket.io documention](https://socket.io/docs/v4/) +- Read more about [SQLAlchemy](https://docs.sqlalchemy.org/en/14/orm/tutorial.html) diff --git a/backend/logic2.py b/backend/logic2.py index 1a7423c..b9f239a 100644 --- a/backend/logic2.py +++ b/backend/logic2.py @@ -1,189 +1,69 @@ -from json import dumps -from typing import Dict from uuid import uuid4 import socketio from gevent import pywsgi +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from models import Base, Player, Room + +# FIXME: Replace later on production sio = socketio.Server( - # TODO: Replace later on production - cors_allowed_origins="*", - logger=False, - engineio_logger=False, - async_mode="gevent", + cors_allowed_origins="*", logger=False, engineio_logger=False, async_mode="gevent" ) app = socketio.WSGIApp(sio) -class Player: - def __init__(self, name: str, sid: str, hearts: int) -> None: - """a class which represent a player - Args: - name (str): the player's name - sid (str): The socket id associated to the user - """ - self.sid = sid - self.name = name - self.points = 0 - self.past_points = 0 - self.answer = None - self.hearts = hearts - - def add_points(self, points): - """Add points to the player. Keep the amount of points added to cancel the answer if needed - Args: - points (int): the amount of points earned - """ - self.points += points - self.past_points = points - - def cancel_current_turn(self): - """Cancel the amount of point earned during this turn""" - self.points -= self.past_points - self.hearts += 1 - self.past_points = 0 - - def to_dict(self) -> dict: - return { - "id": self.sid, - "name": self.name, - "points": self.points, - "past-points": self.past_points, - "answer": self.answer, - "hearts": self.hearts, - } - - -class Setting: - def __init__(self, lives: int = None, timer: int = None) -> None: - self.lives = lives - self.timer = timer - - def to_dict(self): - return {"lives": self.lives, "timer": self.timer} - - -class Room: - def __init__( - self, - players: Dict[str, Player], - admin_id: str, - settings: Setting = Setting(), - locked: bool = False, - answer: str = None, - ) -> None: - """A class which represent a room - Args: - players (Dict[str, Player]): Players in the room - admin_id (str): The admin socket id - settings (Setting, optional): game settings for the room. Defaults to Setting(). - locked (bool, optional): Whether or not new players are allowed to enter the room. Defaults to False. - answer (str, optional): The correct answer for this turn. Defaults to None. - """ - self.players = players - self.admin_id = admin_id - self.settings = settings - self.locked = locked - self.answer = answer - self.questions = 0 - - def is_name_already_used(self, name) -> bool: - """Check whether or not a name is taken by someone else - Args: - name (str): the name to take - Returns: - bool: True if the name is already taken, False otherwise - """ - return name in [p.name for _, p in self.players.items()] - - def add_player(self, name: str, sid: str): - """Add a player to the room object - Args: - name (str): the player's name - sid (str): the socket id with which the player logged in - """ - self.players[sid] = Player(name, sid, int(self.settings.lives)) - - def get_players(self): - """Return a json object with players in the room - Returns: - list: The list of players in the room - """ - return [p.to_dict() for _, p in self.players.items()] - - def get_settings(self) -> dict: - return self.settings.to_dict() - - def next_question(self): - """Increase total question by 1""" - self.questions += 1 - - def to_dict(self): - return { - "admin": self.admin_id, - "players": self.get_players(), - "settings": self.settings.to_dict(), - "locked": self.locked, - "correct-answer": self.answer, - "questions": self.questions, - } - - -ROOMS: Dict[str, Room] = {} -ROOM = None -ROOM_ID = uuid4() - - def make_response(status: str, message: str, **extra): - # TODO: Remove print - # for _, room in ROOMS.items(): - # print(dumps(room.to_dict(), indent=2)) - if ROOM: - print(dumps(ROOM.to_dict(), indent=2)) - return {"status": status, "reason": message, **extra} @sio.on("create-room") -def onCreateRoom(sid, data: dict): +def onCreateRoom(sid: str, data: dict): """Whenever the admin creates a new room + Args: - sid (Any): the client's socket io who emitted the event - data (Dict): data passed alongside the event + sid (str): the client's socket io who emitted the event + data (dict): data passed alongside the event """ - global ROOM lifes = data.get("lifes", None) if lifes: - if ROOM: + query = session.query(Room).filter_by(id=room_id).first() + if query: sio.emit( "create-room", data=make_response("error", "Room is already taken"), to=sid, ) else: - ROOM = Room(players={}, settings=Setting(int(lifes)), admin_id=sid) + room = Room( + id=room_id, + admin_id=sid, + locked=False, + answer="", + question=0, + timer=0, + max_lives=int(lifes), + ) + session.add(room) sio.emit( "create-room", data=make_response("success", "Successfully created the room"), ) - print(dumps(ROOM.to_dict(), indent=2)) - @sio.on("join-room") -def onJoinRoom(sid, data: dict): +def onJoinRoom(sid: str, data: dict): """Whenever a new player tries to join the room + Args: - sid (string): the user's socket iod + sid (str): the user's socket iod data (dict): Data passed alongside the event """ - global ROOM - - print("ROOMID: ", ROOM_ID) - username = data.get("username", None) + room = session.query(Room).filter_by(id=room_id).first() if not username: sio.emit( @@ -191,104 +71,114 @@ def onJoinRoom(sid, data: dict): data=make_response("error", "Unspecified username"), to=sid, ) - elif ROOM and ROOM.locked: + elif room and room.locked: sio.emit( "join-room", data=make_response("error", "Game has already started"), ) else: - ROOM.add_player(username, sid) - # Let the player join the room - sio.enter_room(sid, room=ROOM_ID) + room.players.append( + Player( + sid=sid, + name=username, + points=0, + past_points=0, + answer="", + hearts=room.max_lives, + ) + ) + sio.enter_room(sid, room=room.id) sio.emit( "join-room", data=make_response("success", "Successfully entered the lobby"), to=sid, ) + players = [player.to_dict() for player in room.players] + # Tell the admin a new player joined the room - sio.emit("user-joined", data={"players": ROOM.get_players()}, to=ROOM.admin_id) + sio.emit("user-joined", data={"players": players}, to=room.admin_id) # Tell the players someone joined the room too - sio.emit("user-joined", data={"players": ROOM.get_players()}, room=ROOM_ID) - - print(dumps(ROOM.to_dict(), indent=2)) + sio.emit("user-joined", data={"players": players}, room=room.id) @sio.on("leave-room") -def onLeaveRoom(sid): - global ROOM - - # If the player is the admin - if ROOM.admin_id == sid: - # makes all the players leave the room - sio.emit("leave-room", room=ROOM_ID) +def onLeaveRoom(sid: str): + """Whenever the admin or players leave the game once the leaderboard is shown - # Delete the room - ROOM = None + Args: + sid (str): The emitter's SID + """ + room = session.query(Room).filter_by(id=room_id).first() - # Ping back the admin - sio.emit("leave-room", to=sid) + if room: + # If the emitter is a player then remove him from the table + player = session.query(Player).filter_by(sid=sid).first() + if player: + room.players = [p for p in room.players if p.sid != player.sid] + sio.emit("leave-room", to=sid) + elif room.admin_id == sid: + room.players = [] + sio.emit("leave-room", to=room.admin_id) + # TODO: Remove room from the DB @sio.on("get-game-info") -def get_game_info(sid): +def get_game_info(sid: str): """Send various info to the players once they joined the waiting room + Args: sid (str): the user's socket id """ + room = session.query(Room).filter_by(id=room_id).first() + players = [player.to_dict() for player in room.players] + settings = {"lives": room.max_lives, "timer": room.timer} sio.emit( "get-game-info", - data={ - "players": ROOM.get_players(), - "settings": ROOM.get_settings(), - "question": ROOM.questions, - }, + data={"players": players, "settings": settings, "question": room.question}, to=sid, ) - print(dumps(ROOM.to_dict(), indent=2)) - @sio.on("lock-room") -def lock_room(sid): +def lock_room(sid: str): """When the admin starts the game which means all the players joined the room + Args: - sid (string): the emitter socket id + sid (str): the emitter socket id """ - # Prevent new user from joining the room - ROOM.locked = True + room = session.query(Room).filter_by(id=room_id).first() + room.locked = 1 # Send a response to the admin sio.emit("lock-room-response", to=sid) # Tell the players to be ready - ROOM.next_question() - sio.emit("be-ready", data={"question": ROOM.questions}, room=ROOM_ID) - - print(dumps(ROOM.to_dict(), indent=2)) + room.question += 1 + sio.emit("be-ready", data={"question": room.question}, room=room.id) @sio.on("get-player-info") -def get_player_info(sid): - currentPlayer = [p for _, p in ROOM.players.items() if p.sid == sid][0] +def get_player_info(sid: str): + room = session.query(Room).filter_by(id=room_id).first() + player = session.query(Player).filter_by(sid=sid).first() sio.emit( "get-player-info", data={ - "hearts": ROOM.settings.lives, - "left": currentPlayer.hearts, - "timer": ROOM.settings.timer, - "question": ROOM.questions, + "hearts": room.max_lives, + "left": player.hearts, + "timer": room.timer, + "question": room.question, }, to=sid, ) - print(dumps(ROOM.to_dict(), indent=2)) - @sio.on("set-question-settings") -def set_question_settings(sid, data: dict): +def set_question_settings(sid: str, data: dict): """Whenver the admin finish settings up the nex question + Args: sid (str): the emitter's socket id data (dict): Data passed alongside the event @@ -315,8 +205,11 @@ def set_question_settings(sid, data: dict): to=sid, ) else: - ROOM.answer = answer - ROOM.settings.timer = int(timer) + room = session.query(Room).filter_by(id=room_id).first() + + room.answer = answer + room.timer = int(timer) + # Send the response to the admin sio.emit( "set-question-settings-response", @@ -324,276 +217,126 @@ def set_question_settings(sid, data: dict): to=sid, ) # Send a response the entire room - sio.emit("question-start", data={"timer": timer}, room=ROOM_ID) - - print(dumps(ROOM.to_dict(), indent=2)) + sio.emit("question-start", data={"timer": timer}, room=room.id) @sio.on("user-answer") -def user_answer(sid, data: dict): +def user_answer(sid: str, data: dict): """Whenever a player just answered the question + Args: - sid (str): the player's ocket io + sid (str): the player's socket io data (dict): Data passed alongside the event """ answer = data.get("answer", None) if answer: - print(f"Someone gave an answer: {answer}") + room = session.query(Room).filter_by(id=room_id).first() - # Get the player who just answered - currentPlayer = [p for _, p in ROOM.players.items() if p.sid == sid][0] - currentPlayer.answer = answer + player = session.query(Player).filter_by(sid=sid).first() + player.answer = answer # If the answer is incorrect remove a life if he is still alive - if answer != ROOM.answer: - if currentPlayer.hearts > 0: - currentPlayer.hearts -= 1 + if answer != room.answer: + if player.hearts > 0: + player.hearts -= 1 else: - currentPlayer.add_points(1) + if player.points > 0: + player.past_points += 1 - # Reset the answer given by the player - # currentPlayer.answer = None + player.points += 1 # Send the answers to the admin + dead_players = [p for p in room.players if p.hearts == 0] + players_alive = [p for p in room.players if p.hearts > 0] sio.emit( "update-answers", data={ - "players": len(ROOM.players), + "players": len(room.players), "alive": [ - len( - [ - p - for _, p in ROOM.players.items() - if p.hearts > 0 and p.answer == "A" - ] - ), - len( - [ - p - for _, p in ROOM.players.items() - if p.hearts > 0 and p.answer == "B" - ] - ), - len( - [ - p - for _, p in ROOM.players.items() - if p.hearts > 0 and p.answer == "C" - ] - ), - len( - [ - p - for _, p in ROOM.players.items() - if p.hearts > 0 and p.answer == "D" - ] - ), - len( - [ - p - for _, p in ROOM.players.items() - if p.hearts > 0 and p.answer == "X" - ] - ), + len([p for p in players_alive if p.answer == "A"]), + len([p for p in players_alive if p.answer == "B"]), + len([p for p in players_alive if p.answer == "C"]), + len([p for p in players_alive if p.answer == "D"]), + len([p for p in players_alive if p.answer == "X"]), ], "dead": [ - len( - [ - p - for _, p in ROOM.players.items() - if p.hearts == 0 and p.answer == "A" - ] - ), - len( - [ - p - for _, p in ROOM.players.items() - if p.hearts == 0 and p.answer == "B" - ] - ), - len( - [ - p - for _, p in ROOM.players.items() - if p.hearts == 0 and p.answer == "C" - ] - ), - len( - [ - p - for _, p in ROOM.players.items() - if p.hearts == 0 and p.answer == "D" - ] - ), - len( - [ - p - for _, p in ROOM.players.items() - if p.hearts == 0 and p.answer == "X" - ] - ), + len([p for p in dead_players if p.answer == "A"]), + len([p for p in dead_players if p.answer == "B"]), + len([p for p in dead_players if p.answer == "C"]), + len([p for p in dead_players if p.answer == "D"]), + len([p for p in dead_players if p.answer == "X"]), ], }, - to=ROOM.admin_id, + to=room.admin_id, ) - # Send a response to the player sio.emit( "user-answer", data={ - "correct": answer == ROOM.answer, - "answer": ROOM.answer, - "hearts": ROOM.settings.lives, - "left": currentPlayer.hearts, + "correct": answer == room.answer, + "answer": room.answer, + "hearts": room.max_lives, + "left": player.hearts, }, to=sid, ) else: print(f"An error occured while answering: '{answer}'") - print(dumps(ROOM.to_dict(), indent=2)) - - -@sio.on("get-user-answer") -def user_answer(sid, data: dict): - sio.emit( - "get-update-answers-response", - data={ - "alive": [ - len( - [ - p - for _, p in ROOM.players.items() - if p.hearts > 0 and p.answer == "A" - ] - ), - len( - [ - p - for _, p in ROOM.players.items() - if p.hearts > 0 and p.answer == "B" - ] - ), - len( - [ - p - for _, p in ROOM.players.items() - if p.hearts > 0 and p.answer == "C" - ] - ), - len( - [ - p - for _, p in ROOM.players.items() - if p.hearts > 0 and p.answer == "D" - ] - ), - len( - [ - p - for _, p in ROOM.players.items() - # TODO: Maybe change to None ? - if p.hearts > 0 and p.answer == "" - ] - ), - ], - "dead": [ - len( - [ - p - for _, p in ROOM.players.items() - if p.hearts == 0 and p.answer == "A" - ] - ), - len( - [ - p - for _, p in ROOM.players.items() - if p.hearts == 0 and p.answer == "B" - ] - ), - len( - [ - p - for _, p in ROOM.players.items() - if p.hearts == 0 and p.answer == "C" - ] - ), - len( - [ - p - for _, p in ROOM.players.items() - if p.hearts == 0 and p.answer == "D" - ] - ), - len( - [ - p - for _, p in ROOM.players.items() - if p.hearts == 0 and p.answer == None - ] - ), - ], - }, - to=ROOM.admin_id, - ) - - print(dumps(ROOM.to_dict(), indent=2)) - @sio.on("next-question") -def next_question(sid): - ROOM.next_question() - sio.emit("next-question", room=ROOM_ID) +def next_question(sid: str): + room = session.query(Room).filter_by(id=room_id).first() + room.question += 1 + + # Emit to the admin and players + sio.emit("next-question", to=room.admin_id) + sio.emit("next-question", to=room.id) @sio.on("invalidate") -def invalidate(sid): - # Cancel current turn - for _, p in ROOM.players.items(): - print(f"R: '{ROOM.answer}' & P: '{p.answer}'") - if p.hearts < ROOM.settings.lives: - p.cancel_current_turn() +def invalidate(sid: str): + room = session.query(Room).filter_by(id=room_id).first() - for _, p in ROOM.players.items(): - print(p.to_dict()) + for player in room.players: + # Restore one heart to players + if player.hearts < room.max_lives: + player.points = player.past_points + player.past_points -= 1 + player.hearts += 1 # Update question progress - ROOM.next_question() + room.question += 1 # The the admin the invalidation is done sio.emit("invalidate", to=sid) # Let the players waiting in the waiting room - sio.emit("next-question", room=ROOM_ID) - - print(dumps(ROOM.to_dict(), indent=2)) - - -@sio.on("end-game") -def endgame(sid): - sio.emit("end-game-response", room=ROOM_ID) - - print(dumps(ROOM.to_dict(), indent=2)) + sio.emit("next-question", room=room.id) @sio.on("get-players") -def get_players(sid): - sio.emit("get-players", data={"players": ROOM.get_players()}, to=sid) - - print(dumps(ROOM.to_dict(), indent=2)) +def get_players(sid: str): + room = session.query(Room).filter_by(id=room_id).first() + players = [player.to_dict() for player in room.players] + sio.emit("get-players", data={"players": players}, to=sid) @sio.on("show-leaderboard") -def show_leaderboard(sid): +def show_leaderboard(sid: str): """Show the leaderboard to the entire room + Args: sid (str): the emitter's socket id """ - sio.emit("show-leaderboard") + room = session.query(Room).filter_by(id=room_id).first() - print(dumps(ROOM.to_dict(), indent=2)) + # Emit to the admin + sio.emit("show-leaderboard", to=room.admin_id) + + # Emit to players + sio.emit("show-leaderboard", to=room.id) @sio.event @@ -607,5 +350,27 @@ def disconnect(sid): # TODO: Remove disconnected player from the ROOM and emit events -print("Socket server running: 'localhost:3001'") -pywsgi.WSGIServer(("", 3001), app).serve_forever() +if __name__ == "__main__": + print("Starting:") + print("\t - Creating engine...", end="") + engine = create_engine("sqlite:///:memory:", echo=False) + print("OK !!") + + print("\t - Creating table...", end="") + Base.metadata.create_all(engine) + print("OK !!") + + print("\t - Creating session...", end="") + Session = sessionmaker() + print("OK !!") + + print("\t - Binding to engine...", end="") + Session.configure(bind=engine) + session = Session() + print("OK !!") + + room_id = str(uuid4()) + print(f"\t - ROOM ID: {room_id}\n") + + print("Socket server running: 'localhost:3001'") + pywsgi.WSGIServer(("", 3001), app).serve_forever() diff --git a/backend/models.py b/backend/models.py new file mode 100644 index 0000000..6c76cad --- /dev/null +++ b/backend/models.py @@ -0,0 +1,60 @@ +from sqlalchemy import ForeignKey +from sqlalchemy.orm import declarative_base, relationship +from sqlalchemy import Column, Integer, String + +Base = declarative_base() + +# Mapping classes + + +class Room(Base): + """SQL Table for rooms""" + + __tablename__ = "rooms" + + id = Column(String, primary_key=True) + admin_id = Column(String) + locked = Column(Integer) + answer = Column(String) + timer = Column(Integer) + max_lives = Column(Integer) + question = Column(Integer) + + def __repr__(self) -> str: + r = f"Room(id={self.id}, admin_id={self.admin_id}, locked={self.locked}, answer={self.answer}, timer={self.timer}, max_lives={self.max_lives}, question={self.question})\n" + players = "" + for player in self.players: + players += f" - {repr(player)}\n" + return r + players + + +class Player(Base): + """SQL Table for players""" + + __tablename__ = "players" + + room = Column(String, ForeignKey("rooms.id")) + sid = Column(String, primary_key=True) + name = Column(String) + points = Column(Integer) + past_points = Column(Integer) + answer = Column(String) + hearts = Column(Integer) + + player = relationship("Room", back_populates="players") + + def __repr__(self) -> str: + return f"Player(name={self.name}, sid={self.sid}, points={self.points}, past_points={self.past_points}, answer={self.answer}, hearts={self.hearts})" + + def to_dict(self) -> dict: + return { + "id": self.sid, + "name": self.name, + "points": self.points, + "past-points": self.past_points, + "answer": self.answer, + "hearts": self.hearts, + } + + +Room.players = relationship("Player", order_by=Player.sid, back_populates="player") diff --git a/backend/requirements.txt b/backend/requirements.txt index b16ee8a..44ab25f 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,3 +1,4 @@ python-engineio==3.13.2 python-socketio==4.6.0 -gevent==21.12.0 \ No newline at end of file +gevent==21.12.0 +sqlalchemy==1.4.36 \ No newline at end of file diff --git a/src/views/AdminStatsView.vue b/src/views/AdminStatsView.vue index 58e6247..6a4ff8e 100644 --- a/src/views/AdminStatsView.vue +++ b/src/views/AdminStatsView.vue @@ -12,7 +12,7 @@
- Joueurs spectateur: {{ deadPlayers() }} + Joueurs spectateurs: {{ deadPlayers() }}