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