coding-assignment-iam/app/main.py
2022-09-18 15:48:46 -06:00

212 lines
6.1 KiB
Python

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})