diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..19b1edd --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.vscode/ +.env/ \ No newline at end of file diff --git a/src/app.py b/src/app.py new file mode 100644 index 0000000..b1384fa --- /dev/null +++ b/src/app.py @@ -0,0 +1,81 @@ +from flask import Flask, render_template, request, redirect, url_for, flash, session +from uuid import uuid4 +from .classes import Game +app = Flask(__name__) + + +def find_game_by_id(id:str) -> dict: + games = [] + with open('games.json', 'r') as f: + games = json.load(f) + + index = 0 + for game in games: + if game['id'] == id: + return {"game": game, "idx": index} + index += 1 + return {"error": "Game not found"} + +def start_game(game:dict): + pass + + +@app.route('/') +def index(): + return "Empty for now" + +@app.route('/new//', methods=['POST']) +def new_game(name, players): + # Log new game into json file and return a websocket url for the game + games = [] + with open('games.json', 'r') as f: + games = json.load(f) + + # Create new game + + game = { + "game": name, + "players": players, + "in": [request.remote_addr], + "join": None, + "id": f"{uuid4().hex}", + "status": "waiting", + } + games.append(game) + + with open('games.json', 'w') as f: + json.dump(games, f) + + +@app.route('/join/', methods=['POST']) +def join_game(game_id): + + + # Check if game exists + game = find_game_by_id(game_id) + if "error" in game: + return {"error": "Game not found"} + + # Check if game is full + game, idx = game['game'], game['idx'] + if len(game['in']) >= game['players']: + return {"error": "Game is full"} + + # Add player to game + game['in'].append(request.remote_addr) + + # Check if game is ready + if len(game['in']) == game['players']: + game['status'] = "ready" + start_game(game) + return {"status": "starting"} + + games = [] + with open('games.json', 'r') as f: + games = json.load(f) + games[game.index(game)] = game + with open('games.json', 'w') as f: + json.dump(games, f) + + + diff --git a/src/classes/Game.py b/src/classes/Game.py new file mode 100644 index 0000000..a556179 --- /dev/null +++ b/src/classes/Game.py @@ -0,0 +1,141 @@ +from websockets import serve, ConnectionClosed +import asyncio +from itertools import cycle +import logging +from random import choice, randint +from string import ascii_uppercase + +# TODO: +# I misunderstood everything. I need to put every game inside a data structure, and only interact with it in this class. +# The server shouldn't be here, it should be in app.py +# Also flask_websockets is a thing +# I'm off to do logic now + +class Game: + def __init__(self, players:list, name:str, id:str): + self.players = players + self.toMove = 0 + self.clients = set() + self.players_seen = [] + self.rules = [{"player":p, "rules":[]} for p in players] + self.status = "preparing" + self.name = name + self.id = id + self.end = "" + self.moves = [] + + # Find info about the client from the websocket + def findClient(self, websocket): + for p in self.players_seen: + if p["websocket"] == websocket: + return p["player"] + return None + + # Check if everyone has joined + def isEveryoneIn(self): + return len(self.players_seen) == len(self.players) + + # Check if rules are the same + def areRulesSame(self): + rules = self.rules + return all(x == rules[0] for x in rules) + + # Set an end string + def setEnd(self, length): + letters = ascii_uppercase + return ''.join(choice(letters) for i in range(length)) + + # Add clients + async def handler(self, websocket): + self.clients.add(websocket) + try: + await websocket.wait_closed() + finally: + self.clients.remove(websocket) + + # Messaging system for everyone + async def broadcast(self, message:str): + for websocket in self.clients.copy(): + try: + await websocket.send(message + f" <<< SERVER >>> {findClient(websocket)}") + except ConnectionClosed: + self.clients.remove(websocket) + + async def waitingRoom(self, websocket): + # Wait until everyone joins + while True: + if (self.status == "preparing"): + # Announce that we are waiting for this id + await websocket.broadcast(f"WAITING 4 {self.id}") + + logging.info("Waiting for players to join") + await asyncio.sleep(1) + if self.isEveryoneIn(): + return True + + # Ask player for name + await websocket.send("WHO") + player = await websocket.recv() + if player not in self.players_seen: + self.handler(websocket) + self.players_seen.append({"player":player, "websocket":websocket}) + logging.info(f"Player {player} joined the game") + + # Ask player for rules + await websocket.send("RULES") + rules = await websocket.recv() + logging.info(f"Player {player} sent rules: {rules}") + for rule in rules.split(","): + self.rules[self.players.index(player)]["rules"].append(rule) + + # [Game preparation](https://git.confest.im/boyan_k/game_server/media/branch/master/media/game_prep.png) + async def prepareGame(self): + # Wait for everyone to join + async with serve(self.waitingRoom, "0.0.0.0", 8765): + await asyncio.Future() + + # Check if rules are valid + if not self.areRulesSame(): + await self.broadcast("RULES_MISMATCH") + return False + + # Start game + self.status = "running" + self.end = self.setEnd(randint(5, 10)) + await self.broadcast("START: " + self.end) + self.toMove = cycle(self.clients) + return True + + + async def turnBased(self, websocket): + # Wait for player to send their move + await websocket.send("MOVE") + move = await websocket.recv() + player = self.findClient(websocket) + logging.info(f"Player {player} sent move: {move}") + + # TODO: Include some sort of validation + + self.moves.append({"player":player, "move":move}) + return player, move + + async def runGame(self): + # Run game + while True: + client = self.toMove.__next__() + p, m = await self.turnBased(client) + # We assume that the person who ended is the one who called his loss + if m == self.end: + # We remove them + await self.broadcast(f"{p} OUT") + self.clients.remove(client) + # We check if there is only one player left, in which case, he is the winner + if len(self.clients) == 1: + await self.broadcast(f"{self.findClient(self.clients[0])} WINS") + return True + self.toMove = cycle(self.clients) + + + + + diff --git a/src/games.json b/src/games.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/src/games.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/src/known_games.json b/src/known_games.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/src/known_games.json @@ -0,0 +1 @@ +[] \ No newline at end of file