Add basic backend logic and frontend templates/css

This commit is contained in:
Joey Yakimowich-Payne 2022-09-18 14:47:07 -06:00
commit 8864bacd1e
8 changed files with 324 additions and 22 deletions

212
app/main.py Normal file
View 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})

27
html/error.html Normal file
View file

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html>
<head>
<title>Error | Rock Paper Scissors</title>
</head>
<link href="{{ url_for('static', path='/css/bulma.min.css') }}" rel="stylesheet">
<body>
<section class="section">
<div class="container">
<div class="columns is-vcentered">
<div class="column">
<article class="message is-danger">
<div class="message-header">
<p>Error</p>
</div>
<div class="message-body">
{{ message }}
</div>
</article>
</div>
</div>
</div>
</section>
<script type="text/javascript">
</script>
</body>
</html>

14
html/game.html Normal file
View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<title>Rock Paper Scissors</title>
</head>
<link href="{{ url_for('static', path='/css/bulma.min.css') }}" rel="stylesheet">
<body>
<div>
Hello {{ game.player1.name }} and {{ game.player2.name }}
</div>
<script type="text/javascript">
</script>
</body>
</html>

69
html/index.html Normal file
View file

@ -0,0 +1,69 @@
<!DOCTYPE html>
<html>
<head>
<title>Rock Paper Scissors</title>
</head>
<link href="{{ url_for('static', path='/css/bulma.min.css') }}" rel="stylesheet">
<body>
<section class="section">
<div class="container">
<div class="columns is-vcentered">
<div class="column">
<div class="field">
<label class="label">Player 1 Name</label>
<div class="control">
<input id="player1-name" class="input" type="text" placeholder="Name">
</div>
</div>
<div class="field">
<label class="label">Player 2 Name (Optional)</label>
<div class="control">
<input id="player2-name" class="input" type="text" placeholder="Name">
</div>
</div>
<div class="field">
<div class="control">
<button id="play-btn" class="button is-link">Play Game!</button>
</div>
</div>
</div>
</div>
<div class="columns is-vcentered">
<div class="column">
<div class="field is-horizontal is-justify-content-center">
<div class="field-body">
<div class="field-label">
<label class="label">OR</label>
</div>
<div class="field has-addons">
<div class="control">
</div>
</div>
</div>
</div>
</div>
</div>
<div class="columns is-vcentered">
<div class="column">
<div class="field">
<label class="label">Game Code</label>
<div class="control">
<input id="game-code" class="input" type="text" placeholder="Game Code: JDPB">
</div>
</div>
<div class="field">
<div class="control">
<button id="resume-btn" class="button is-link">Resume Game!</button>
</div>
</div>
</div>
</div>
</div>
</section>
<script type="text/javascript">
</script>
</body>
</html>

View file

@ -1,11 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Rock Paper Scissors</title>
</head>
<body>
<div>
Hello world
</div>
</body>
</html>

10
main.py
View file

@ -1,10 +0,0 @@
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
app = FastAPI()
html = open("index.html").read()
@app.get("/")
async def root():
return HTMLResponse(html)

2
run.sh
View file

@ -1 +1 @@
uvicorn main:app --host 0.0.0.0 --port 8080
uvicorn app.main:app --host 0.0.0.0 --port 8080

1
static/css/bulma.min.css vendored Normal file

File diff suppressed because one or more lines are too long