From 41a513f58158d4956c24e6f184b0067c4d0792b5 Mon Sep 17 00:00:00 2001
From: Boyan <boyan@confest.im>
Date: Thu, 2 Nov 2023 16:51:38 +0100
Subject: [PATCH] I wrote a whole Game class, might be unnecessary.

---
 .gitignore           |   2 +
 src/app.py           |  81 +++++++++++++++++++++++++
 src/classes/Game.py  | 141 +++++++++++++++++++++++++++++++++++++++++++
 src/games.json       |   1 +
 src/known_games.json |   1 +
 5 files changed, 226 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 src/app.py
 create mode 100644 src/classes/Game.py
 create mode 100644 src/games.json
 create mode 100644 src/known_games.json

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/<str:name>/<int:players>', 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/<uuid:game_id>', 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