Add basic backend logic and frontend templates/css
This commit is contained in:
parent
03fd03af5f
commit
8864bacd1e
8 changed files with 324 additions and 22 deletions
212
app/main.py
Normal file
212
app/main.py
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
import pickle
|
||||
import os
|
||||
import string
|
||||
import random
|
||||
from uuid import uuid4
|
||||
|
||||
from fastapi import FastAPI, Request, status, Response
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import JSONResponse
|
||||
from pydantic import BaseModel
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||
|
||||
templates = Jinja2Templates(directory="html")
|
||||
|
||||
|
||||
class PlayersInfo(BaseModel):
|
||||
player1_name: str
|
||||
player2_name: str | None
|
||||
|
||||
|
||||
class GameSaveInfo(BaseModel):
|
||||
game_code: str
|
||||
|
||||
|
||||
class IncreaseScoreInfo(BaseModel):
|
||||
player_name: str
|
||||
game_code: str
|
||||
|
||||
|
||||
class GameURLInfo(BaseModel):
|
||||
game_url: str
|
||||
|
||||
|
||||
class PlayerNotFoundError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class GameNotFoundError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Player:
|
||||
def __init__(self, name: str, id=uuid4()):
|
||||
self.name = name
|
||||
self.id = id
|
||||
self.score = 0
|
||||
|
||||
|
||||
class CPUPlayer(Player):
|
||||
def __init__(self, id=uuid4()):
|
||||
super().__init__("CPUPlayer", id)
|
||||
|
||||
|
||||
class Game:
|
||||
"""A class to keep track of the score between player 1 and player 2"""
|
||||
|
||||
def __init__(self, code: str, player1: Player, player2: Player):
|
||||
self.code = code
|
||||
|
||||
self.players = {player1.name: player1, player2.name: player2}
|
||||
self.player1 = player1
|
||||
self.player2 = player2
|
||||
|
||||
def get_player(self, player_name: str) -> Player:
|
||||
if player_name not in self.players:
|
||||
raise PlayerNotFoundError()
|
||||
|
||||
return self.players[player_name]
|
||||
|
||||
def increase_score(self, player_name: str):
|
||||
player = self.get_player(player_name)
|
||||
player.score += 1
|
||||
|
||||
def set_score(self, player_name: str, score: int):
|
||||
player = self.get_player(player_name)
|
||||
player.score = score
|
||||
|
||||
|
||||
class GameManager:
|
||||
save_path = "saves"
|
||||
save_file = "{game_code}.savefile"
|
||||
|
||||
def __init__(self):
|
||||
self.games: dict[str, Game] = {}
|
||||
self.game_code_index = 0
|
||||
self.load_games()
|
||||
|
||||
def load_games(self):
|
||||
if not os.path.exists(self.save_path):
|
||||
return
|
||||
|
||||
for path in os.listdir(self.save_path):
|
||||
if path.endswith(".savefile"):
|
||||
# Pickle is not safe and should be replaced with a suitable
|
||||
# alternative
|
||||
with open(os.path.join(self.save_path, path), "rb") as f:
|
||||
self.games.update(pickle.load(f))
|
||||
|
||||
def get_game(self, game_code: str) -> Game:
|
||||
if game_code not in self.games:
|
||||
raise GameNotFoundError()
|
||||
return self.games[game_code]
|
||||
|
||||
def new_game(self, player1: Player, player2: Player) -> Game:
|
||||
code = GAME_CODES[self.game_code_index]
|
||||
self.games[code] = Game(code, player1, player2)
|
||||
self.game_code_index += 1
|
||||
return self.games[code]
|
||||
|
||||
async def save_game(self, game_code: str):
|
||||
"""Save the game to disk"""
|
||||
# ideally, this would be saved to SQL or similar
|
||||
# but for the sake of time, just save it to a file
|
||||
os.makedirs(self.save_path, exist_ok=True)
|
||||
|
||||
game_filepath = os.path.join(
|
||||
self.save_path, self.save_file.format(game_code=game_code)
|
||||
)
|
||||
|
||||
# Pickle is not safe and should be replaced with a suitable
|
||||
# alternative
|
||||
with open(game_filepath, "wb+") as f:
|
||||
pickle.dump({game_code: self.get_game(game_code)}, f)
|
||||
|
||||
|
||||
def create_game_codes() -> list[str]:
|
||||
"""Generate all permutations of all letters for game codes once"""
|
||||
codes: list[str] = []
|
||||
|
||||
# Make sure they are random so that people can't predict
|
||||
# which games will be taken
|
||||
chars1 = list(string.ascii_uppercase)
|
||||
random.shuffle(chars1)
|
||||
chars2 = list(string.ascii_uppercase)
|
||||
random.shuffle(chars2)
|
||||
chars3 = list(string.ascii_uppercase)
|
||||
random.shuffle(chars3)
|
||||
chars4 = list(string.ascii_uppercase)
|
||||
random.shuffle(chars4)
|
||||
|
||||
for ch1 in chars1:
|
||||
for ch2 in chars2:
|
||||
for ch3 in chars3:
|
||||
for ch4 in chars4:
|
||||
code = f"{ch1}{ch2}{ch3}{ch4}"
|
||||
if code not in GAME_MANAGER.games:
|
||||
codes.append(code)
|
||||
return codes
|
||||
|
||||
|
||||
GAME_MANAGER = GameManager()
|
||||
GAME_CODES: list[str] = create_game_codes()
|
||||
|
||||
|
||||
@app.post("/save-game", response_class=HTMLResponse, status_code=200)
|
||||
async def save_game(_: Request, game_save_info: GameSaveInfo, resp: Response):
|
||||
"""Save the game to reload later"""
|
||||
try:
|
||||
await GAME_MANAGER.save_game(game_save_info.game_code)
|
||||
except GameNotFoundError:
|
||||
resp.status_code = status.HTTP_400_BAD_REQUEST
|
||||
|
||||
|
||||
@app.post("/increase-score", response_class=HTMLResponse, status_code=200)
|
||||
async def increase_score(_: Request, inc_score_info: IncreaseScoreInfo, resp: Response):
|
||||
try:
|
||||
game = GAME_MANAGER.get_game(inc_score_info.game_code)
|
||||
game.increase_score(inc_score_info.player_name)
|
||||
except (PlayerNotFoundError, GameNotFoundError):
|
||||
resp.status_code = status.HTTP_400_BAD_REQUEST
|
||||
|
||||
|
||||
@app.post("/start-game", response_class=HTMLResponse)
|
||||
async def start_game(request: Request, players_info: PlayersInfo):
|
||||
player2 = (
|
||||
Player(players_info.player2_name)
|
||||
if players_info.player2_name is not None
|
||||
else CPUPlayer()
|
||||
)
|
||||
|
||||
game = GAME_MANAGER.new_game(Player(players_info.player1_name), player2)
|
||||
|
||||
return JSONResponse(
|
||||
content=jsonable_encoder(
|
||||
GameURLInfo(game_url=request.url_for("game") + f"?game_code={game.code}")
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@app.get("/game", response_class=HTMLResponse)
|
||||
async def game(request: Request, game_code: str):
|
||||
try:
|
||||
game = GAME_MANAGER.get_game(game_code)
|
||||
return templates.TemplateResponse(
|
||||
"game.html", {"request": request, "game": game}
|
||||
)
|
||||
except GameNotFoundError:
|
||||
return templates.TemplateResponse(
|
||||
"error.html",
|
||||
{"request": request, "message": f"Game {game_code} not found!"},
|
||||
)
|
||||
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
async def root(request: Request):
|
||||
return templates.TemplateResponse("index.html", {"request": request})
|
||||
Loading…
Add table
Add a link
Reference in a new issue