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})
|
||||||
27
html/error.html
Normal file
27
html/error.html
Normal 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
14
html/game.html
Normal 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
69
html/index.html
Normal 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>
|
||||||
11
index.html
11
index.html
|
|
@ -1,11 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Rock Paper Scissors</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div>
|
|
||||||
Hello world
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
10
main.py
10
main.py
|
|
@ -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
2
run.sh
|
|
@ -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
1
static/css/bulma.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue